Compare commits
15 Commits
main
...
bet-timed-
Author | SHA1 | Date | |
---|---|---|---|
|
583a797a61 | ||
|
139902f15e | ||
|
668d111973 | ||
|
31e1f4ba28 | ||
|
f3092892d8 | ||
|
1adfb874c3 | ||
|
3aad05e2a6 | ||
|
13de5de24d | ||
|
1cfbd86bd0 | ||
|
ba9411161d | ||
|
fd49ccea39 | ||
|
1b17d3a102 | ||
|
98510f233d | ||
|
d1fab9937f | ||
|
3d368216b2 |
|
@ -30,17 +30,17 @@ export function formatPercent(zeroToOne: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eg 1234567.89 => 1.23M; 5678 => 5.68K
|
// Eg 1234567.89 => 1.23M; 5678 => 5.68K
|
||||||
export function formatLargeNumber(num: number, sigfigs = 3): string {
|
export function formatLargeNumber(num: number, sigfigs = 2): string {
|
||||||
const absNum = Math.abs(num)
|
const absNum = Math.abs(num)
|
||||||
if (absNum < 1000) {
|
if (absNum < 1000) {
|
||||||
return num.toPrecision(sigfigs)
|
return '' + Number(num.toPrecision(sigfigs))
|
||||||
}
|
}
|
||||||
|
|
||||||
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
|
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
|
||||||
const suffixIdx = Math.floor(Math.log10(absNum) / 3)
|
const suffixIdx = Math.floor(Math.log10(absNum) / 3)
|
||||||
const suffixStr = suffix[suffixIdx]
|
const suffixStr = suffix[suffixIdx]
|
||||||
const numStr = (num / Math.pow(10, 3 * suffixIdx)).toPrecision(sigfigs)
|
const numStr = (num / Math.pow(10, 3 * suffixIdx)).toPrecision(sigfigs)
|
||||||
return `${numStr}${suffixStr}`
|
return `${Number(numStr)}${suffixStr}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toCamelCase(words: string) {
|
export function toCamelCase(words: string) {
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { formatLargeNumber, formatPercent } from 'common/util/format'
|
import {
|
||||||
|
formatLargeNumber,
|
||||||
|
formatMoney,
|
||||||
|
formatPercent,
|
||||||
|
} from 'common/util/format'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
contractPath,
|
contractPath,
|
||||||
|
@ -25,8 +29,14 @@ import {
|
||||||
OUTCOME_TO_COLOR,
|
OUTCOME_TO_COLOR,
|
||||||
} from '../outcome-label'
|
} from '../outcome-label'
|
||||||
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
||||||
import { AbbrContractDetails } from './contract-details'
|
import { AvatarDetails, MiscDetails } from './contract-details'
|
||||||
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
||||||
|
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
|
||||||
|
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { CheckIcon, XIcon } from '@heroicons/react/solid'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { APIError, placeBet } from 'web/lib/firebase/api-call'
|
||||||
|
|
||||||
// Return a number from 0 to 1 for this contract
|
// Return a number from 0 to 1 for this contract
|
||||||
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
||||||
|
@ -50,6 +60,9 @@ function getNumericScale(contract: NumericContract) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColor(contract: Contract) {
|
function getColor(contract: Contract) {
|
||||||
|
// TODO: Not sure why eg green-400 doesn't work here; try upgrading Tailwind
|
||||||
|
// TODO: Try injecting a gradient here
|
||||||
|
// return 'primary'
|
||||||
const { resolution } = contract
|
const { resolution } = contract
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
return (
|
return (
|
||||||
|
@ -83,35 +96,121 @@ export function ContractCard(props: {
|
||||||
const prob = getProb(contract)
|
const prob = getProb(contract)
|
||||||
const color = getColor(contract)
|
const color = getColor(contract)
|
||||||
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
||||||
const showTopBar = prob >= 0.5 || marketClosed
|
|
||||||
|
// TODO: switch to useContract after you place a bet on it
|
||||||
|
|
||||||
|
function betToast(outcome: string) {
|
||||||
|
let canceled = false
|
||||||
|
const toastId = toast.custom(
|
||||||
|
<BetToast
|
||||||
|
outcome={outcome}
|
||||||
|
seconds={3}
|
||||||
|
onToastFinish={onToastFinish}
|
||||||
|
onToastCancel={onToastCancel}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
duration: 3000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function onToastCancel() {
|
||||||
|
toast.remove(toastId)
|
||||||
|
canceled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onToastFinish() {
|
||||||
|
if (canceled) return
|
||||||
|
console.log('Finishing toast')
|
||||||
|
toast.remove(toastId)
|
||||||
|
placeBet({
|
||||||
|
amount: 10,
|
||||||
|
outcome,
|
||||||
|
contractId: contract.id,
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
// Success
|
||||||
|
console.log('placed bet. Result:', r)
|
||||||
|
toast.success('Bet placed!', { duration: 1000 })
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
// Failure
|
||||||
|
if (e instanceof APIError) {
|
||||||
|
toast.error(e.toString(), { duration: 1000 })
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Could not place bet')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative gap-3 rounded-lg bg-white p-6 pr-7 shadow-md hover:bg-gray-100',
|
'relative gap-3 rounded-lg bg-white py-4 pl-6 pr-5 shadow-md hover:cursor-pointer hover:bg-gray-100',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<Col className="relative flex-1 gap-3 pr-1">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'peer absolute -left-6 -top-4 -bottom-4 z-10',
|
||||||
|
// Hack: Extend the clickable area for closed markets
|
||||||
|
marketClosed ? 'right-[-6.5rem]' : 'right-0'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Link href={contractPath(contract)}>
|
<Link href={contractPath(contract)}>
|
||||||
<a className="absolute left-0 right-0 top-0 bottom-0" />
|
<a className="absolute top-0 left-0 right-0 bottom-0" />
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
<AbbrContractDetails
|
<AvatarDetails contract={contract} />
|
||||||
contract={contract}
|
|
||||||
showHotVolume={showHotVolume}
|
|
||||||
showCloseTime={showCloseTime}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Row className={clsx('justify-between gap-4')}>
|
|
||||||
<Col className="gap-3">
|
|
||||||
<p
|
<p
|
||||||
className="break-words font-medium text-indigo-700"
|
className="break-words font-medium text-indigo-700 peer-hover:underline peer-hover:decoration-indigo-400 peer-hover:decoration-2"
|
||||||
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
|
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
|
||||||
>
|
>
|
||||||
{question}
|
{question}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
|
<FreeResponseTopAnswer
|
||||||
|
contract={contract as FullContract<DPM, FreeResponse>}
|
||||||
|
truncate="long"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MiscDetails
|
||||||
|
contract={contract}
|
||||||
|
showHotVolume={showHotVolume}
|
||||||
|
showCloseTime={showCloseTime}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
<Col className="relative -my-4 -mr-5 min-w-[6rem] justify-center gap-2 pr-5 pl-3 align-middle">
|
||||||
|
{!marketClosed && (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="peer absolute top-0 left-0 right-0 h-[50%]"
|
||||||
|
onClick={() => betToast('YES')}
|
||||||
|
></div>
|
||||||
|
<div className="my-1 text-center text-xs text-transparent peer-hover:text-gray-400">
|
||||||
|
{formatMoney(20)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{contract.createdTime % 3 == 0 ? (
|
||||||
|
<TriangleFillIcon
|
||||||
|
className={clsx(
|
||||||
|
'mx-auto h-5 w-5 text-opacity-60 peer-hover:text-opacity-100',
|
||||||
|
`text-${color}`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TriangleFillIcon className="mx-auto h-5 w-5 text-gray-200 peer-hover:text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{outcomeType === 'BINARY' && (
|
{outcomeType === 'BINARY' && (
|
||||||
<BinaryResolutionOrChance
|
<BinaryResolutionOrChance
|
||||||
className="items-center"
|
className="items-center"
|
||||||
|
@ -125,7 +224,6 @@ export function ContractCard(props: {
|
||||||
contract={contract as NumericContract}
|
contract={contract as NumericContract}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Row>
|
|
||||||
|
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
<FreeResponseResolutionOrChance
|
<FreeResponseResolutionOrChance
|
||||||
|
@ -135,6 +233,30 @@ export function ContractCard(props: {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!marketClosed && (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="peer absolute bottom-0 left-0 right-0 h-[50%]"
|
||||||
|
onClick={() => betToast('NO')}
|
||||||
|
></div>
|
||||||
|
{contract.createdTime % 3 == 2 ? (
|
||||||
|
<TriangleDownFillIcon
|
||||||
|
className={clsx(
|
||||||
|
'mx-auto h-5 w-5 text-opacity-60 peer-hover:text-opacity-100',
|
||||||
|
`text-${color}`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TriangleDownFillIcon className="mx-auto h-5 w-5 text-gray-200 peer-hover:text-gray-400" />
|
||||||
|
)}
|
||||||
|
<div className="my-1 text-center text-xs text-transparent peer-hover:text-gray-400">
|
||||||
|
{formatMoney(20)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute right-0 top-0 w-2 rounded-tr-md',
|
'absolute right-0 top-0 w-2 rounded-tr-md',
|
||||||
|
@ -182,15 +304,31 @@ export function BinaryResolutionOrChance(props: {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={textColor}>{getBinaryProbPercent(contract)}</div>
|
<div className={textColor}>{getBinaryProbPercent(contract)}</div>
|
||||||
<div className={clsx(textColor, large ? 'text-xl' : 'text-base')}>
|
|
||||||
chance
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FreeResponseTopAnswer(props: {
|
||||||
|
contract: FreeResponseContract
|
||||||
|
truncate: 'short' | 'long' | 'none'
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, truncate } = props
|
||||||
|
const { resolution } = contract
|
||||||
|
|
||||||
|
const topAnswer = getTopAnswer(contract)
|
||||||
|
|
||||||
|
return topAnswer ? (
|
||||||
|
<AnswerLabel
|
||||||
|
className="!text-gray-600"
|
||||||
|
answer={topAnswer}
|
||||||
|
truncate={truncate}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
export function FreeResponseResolutionOrChance(props: {
|
export function FreeResponseResolutionOrChance(props: {
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
|
@ -217,16 +355,10 @@ export function FreeResponseResolutionOrChance(props: {
|
||||||
) : (
|
) : (
|
||||||
topAnswer && (
|
topAnswer && (
|
||||||
<Row className="items-center gap-6">
|
<Row className="items-center gap-6">
|
||||||
<AnswerLabel
|
|
||||||
className="!text-gray-600"
|
|
||||||
answer={topAnswer}
|
|
||||||
truncate={truncate}
|
|
||||||
/>
|
|
||||||
<Col className={clsx('text-3xl', textColor)}>
|
<Col className={clsx('text-3xl', textColor)}>
|
||||||
<div>
|
<div>
|
||||||
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
|
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base">chance</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
|
@ -241,6 +373,7 @@ export function NumericResolutionOrExpectation(props: {
|
||||||
}) {
|
}) {
|
||||||
const { contract, className } = props
|
const { contract, className } = props
|
||||||
const { resolution } = contract
|
const { resolution } = contract
|
||||||
|
const textColor = `text-${getColor(contract)}`
|
||||||
|
|
||||||
const resolutionValue =
|
const resolutionValue =
|
||||||
contract.resolutionValue ?? getValueFromBucket(resolution ?? '', contract)
|
contract.resolutionValue ?? getValueFromBucket(resolution ?? '', contract)
|
||||||
|
@ -254,12 +387,72 @@ export function NumericResolutionOrExpectation(props: {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="text-3xl text-blue-400">
|
<div className={clsx('text-3xl', textColor)}>
|
||||||
{formatLargeNumber(getExpectedValue(contract))}
|
{formatLargeNumber(getExpectedValue(contract))}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base text-blue-400">expected</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BetToast(props: {
|
||||||
|
outcome: string
|
||||||
|
seconds: number
|
||||||
|
onToastFinish: () => void
|
||||||
|
onToastCancel: () => void
|
||||||
|
}) {
|
||||||
|
const { outcome, seconds, onToastFinish, onToastCancel } = props
|
||||||
|
|
||||||
|
// Track the number of seconds left, starting with durationMs
|
||||||
|
const [secondsLeft, setSecondsLeft] = useState(seconds)
|
||||||
|
console.log('renderings using', secondsLeft)
|
||||||
|
|
||||||
|
// Update the secondsLeft state every second
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setSecondsLeft((seconds) => seconds - 1)
|
||||||
|
}, 1000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (secondsLeft <= 0) {
|
||||||
|
console.log('finishing')
|
||||||
|
onToastFinish()
|
||||||
|
// return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex w-0 flex-1 justify-between">
|
||||||
|
<p className="w-0 flex-1 text-sm font-medium text-gray-900">
|
||||||
|
Betting M$10 on {outcome} in {secondsLeft}s
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onToastCancel}
|
||||||
|
className="ml-3 flex-shrink-0 rounded-md bg-white text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* <div className="ml-4 flex flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
onClick={() => {
|
||||||
|
// TODO
|
||||||
|
// setShow(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,9 @@ import {
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
CurrencyDollarIcon,
|
CurrencyDollarIcon,
|
||||||
TrendingUpIcon,
|
TrendingUpIcon,
|
||||||
|
StarIcon,
|
||||||
} from '@heroicons/react/outline'
|
} from '@heroicons/react/outline'
|
||||||
|
import { StarIcon as SolidStarIcon } from '@heroicons/react/solid'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { UserLink } from '../user-page'
|
import { UserLink } from '../user-page'
|
||||||
|
@ -26,20 +28,13 @@ import NewContractBadge from '../new-contract-badge'
|
||||||
import { CATEGORY_LIST } from 'common/categories'
|
import { CATEGORY_LIST } from 'common/categories'
|
||||||
import { TagsList } from '../tags-list'
|
import { TagsList } from '../tags-list'
|
||||||
|
|
||||||
export function AbbrContractDetails(props: {
|
export function MiscDetails(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
showHotVolume?: boolean
|
showHotVolume?: boolean
|
||||||
showCloseTime?: boolean
|
showCloseTime?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { contract, showHotVolume, showCloseTime } = props
|
const { contract, showHotVolume, showCloseTime } = props
|
||||||
const {
|
const { volume, volume24Hours, closeTime, tags } = contract
|
||||||
volume,
|
|
||||||
volume24Hours,
|
|
||||||
creatorName,
|
|
||||||
creatorUsername,
|
|
||||||
closeTime,
|
|
||||||
tags,
|
|
||||||
} = contract
|
|
||||||
const { volumeLabel } = contractMetrics(contract)
|
const { volumeLabel } = contractMetrics(contract)
|
||||||
// Show at most one category that this contract is tagged by
|
// Show at most one category that this contract is tagged by
|
||||||
const categories = CATEGORY_LIST.filter((category) =>
|
const categories = CATEGORY_LIST.filter((category) =>
|
||||||
|
@ -47,26 +42,20 @@ export function AbbrContractDetails(props: {
|
||||||
).slice(0, 1)
|
).slice(0, 1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={clsx('gap-2 text-sm text-gray-500')}>
|
<Row className="items-center gap-3 text-sm text-gray-400">
|
||||||
<Row className="items-center justify-between">
|
{/* {contract.createdTime % 3 == 1 ? (
|
||||||
<Row className="items-center gap-2">
|
<SolidStarIcon className="h-6 w-6 text-indigo-600" />
|
||||||
<Avatar
|
) : (
|
||||||
username={creatorUsername}
|
<StarIcon className="h-6 w-6 text-gray-400" />
|
||||||
avatarUrl={contract.creatorAvatarUrl}
|
)} */}
|
||||||
size={6}
|
|
||||||
/>
|
|
||||||
<UserLink name={creatorName} username={creatorUsername} />
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row className="gap-3 text-gray-400">
|
|
||||||
{categories.length > 0 && (
|
{categories.length > 0 && (
|
||||||
<TagsList className="text-gray-400" tags={categories} noLabel />
|
<TagsList className="text-gray-400" tags={categories} noLabel />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showHotVolume ? (
|
{showHotVolume ? (
|
||||||
<Row className="gap-0.5">
|
<Row className="gap-0.5">
|
||||||
<TrendingUpIcon className="h-5 w-5" />{' '}
|
<TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)}
|
||||||
{formatMoney(volume24Hours)}
|
|
||||||
</Row>
|
</Row>
|
||||||
) : showCloseTime ? (
|
) : showCloseTime ? (
|
||||||
<Row className="gap-0.5">
|
<Row className="gap-0.5">
|
||||||
|
@ -80,6 +69,41 @@ export function AbbrContractDetails(props: {
|
||||||
<NewContractBadge />
|
<NewContractBadge />
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AvatarDetails(props: { contract: Contract }) {
|
||||||
|
const { contract } = props
|
||||||
|
const { creatorName, creatorUsername } = contract
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row className="items-center gap-2 text-sm text-gray-500">
|
||||||
|
<Avatar
|
||||||
|
username={creatorUsername}
|
||||||
|
avatarUrl={contract.creatorAvatarUrl}
|
||||||
|
size={6}
|
||||||
|
/>
|
||||||
|
<UserLink name={creatorName} username={creatorUsername} />
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AbbrContractDetails(props: {
|
||||||
|
contract: Contract
|
||||||
|
showHotVolume?: boolean
|
||||||
|
showCloseTime?: boolean
|
||||||
|
}) {
|
||||||
|
const { contract, showHotVolume, showCloseTime } = props
|
||||||
|
return (
|
||||||
|
<Col className="gap-2">
|
||||||
|
<Row className="items-center justify-between">
|
||||||
|
<AvatarDetails contract={contract} />
|
||||||
|
|
||||||
|
<MiscDetails
|
||||||
|
contract={contract}
|
||||||
|
showHotVolume={showHotVolume}
|
||||||
|
showCloseTime={showCloseTime}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
@ -93,7 +117,7 @@ export function ContractDetails(props: {
|
||||||
}) {
|
}) {
|
||||||
const { contract, bets, isCreator, disabled } = props
|
const { contract, bets, isCreator, disabled } = props
|
||||||
const { closeTime, creatorName, creatorUsername } = contract
|
const { closeTime, creatorName, creatorUsername } = contract
|
||||||
const { volumeLabel, createdDate, resolvedDate } = contractMetrics(contract)
|
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">
|
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">
|
||||||
|
|
|
@ -84,11 +84,12 @@ export function FreeResponseOutcomeLabel(props: {
|
||||||
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
||||||
return (
|
return (
|
||||||
<FreeResponseAnswerToolTip text={chosen.text}>
|
<FreeResponseAnswerToolTip text={chosen.text}>
|
||||||
<AnswerLabel
|
<span className="text-blue-400">TOP</span>
|
||||||
|
{/* <AnswerLabel
|
||||||
answer={chosen}
|
answer={chosen}
|
||||||
truncate={truncate}
|
truncate={truncate}
|
||||||
className={answerClassName}
|
className={answerClassName}
|
||||||
/>
|
/> */}
|
||||||
</FreeResponseAnswerToolTip>
|
</FreeResponseAnswerToolTip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -117,7 +118,7 @@ export function ProbLabel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiLabel() {
|
export function MultiLabel() {
|
||||||
return <span className="text-blue-400">MULTI</span>
|
return <span className="text-blue-400">MANY</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProbPercentLabel(props: { prob: number }) {
|
export function ProbPercentLabel(props: { prob: number }) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { BottomNavBar } from './nav/nav-bar'
|
import { BottomNavBar } from './nav/nav-bar'
|
||||||
import Sidebar from './nav/sidebar'
|
import Sidebar from './nav/sidebar'
|
||||||
|
import { Toaster } from 'react-hot-toast'
|
||||||
|
|
||||||
export function Page(props: {
|
export function Page(props: {
|
||||||
margin?: boolean
|
margin?: boolean
|
||||||
|
@ -20,6 +21,7 @@ export function Page(props: {
|
||||||
)}
|
)}
|
||||||
style={suspend ? visuallyHiddenStyle : undefined}
|
style={suspend ? visuallyHiddenStyle : undefined}
|
||||||
>
|
>
|
||||||
|
<Toaster />
|
||||||
<Sidebar className="sticky top-4 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:block" />
|
<Sidebar className="sticky top-4 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:block" />
|
||||||
<main
|
<main
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|
17
web/lib/icons/triangle-down-fill-icon.tsx
Normal file
17
web/lib/icons/triangle-down-fill-icon.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Icon from Bootstrap: https://icons.getbootstrap.com/
|
||||||
|
export default function TriangleDownFillIcon(props: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
className={props.className}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
transform="rotate(-180 8 8)"
|
||||||
|
d="M7.022 1.566a1.13 1.13 0 0 1 1.96 0l6.857 11.667c.457.778-.092 1.767-.98 1.767H1.144c-.889 0-1.437-.99-.98-1.767L7.022 1.566z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
16
web/lib/icons/triangle-fill-icon.tsx
Normal file
16
web/lib/icons/triangle-fill-icon.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Icon from Bootstrap: https://icons.getbootstrap.com/
|
||||||
|
export default function TriangleFillIcon(props: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
className={props.className}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M7.022 1.566a1.13 1.13 0 0 1 1.96 0l6.857 11.667c.457.778-.092 1.767-.98 1.767H1.144c-.889 0-1.437-.99-.98-1.767L7.022 1.566z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
|
@ -35,6 +35,7 @@
|
||||||
"react-confetti": "6.0.1",
|
"react-confetti": "6.0.1",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-expanding-textarea": "2.3.5",
|
"react-expanding-textarea": "2.3.5",
|
||||||
|
"react-hot-toast": "^2.2.0",
|
||||||
"react-instantsearch-hooks-web": "6.24.1"
|
"react-instantsearch-hooks-web": "6.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -3056,6 +3056,11 @@ globby@^11.0.4:
|
||||||
merge2 "^1.4.1"
|
merge2 "^1.4.1"
|
||||||
slash "^3.0.0"
|
slash "^3.0.0"
|
||||||
|
|
||||||
|
goober@^2.1.1:
|
||||||
|
version "2.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.9.tgz#0faee08fab1a5d55b23e9ec043bb5a1b46fa025a"
|
||||||
|
integrity sha512-PAtnJbrWtHbfpJUIveG5PJIB6Mc9Kd0gimu9wZwPyA+wQUSeOeA4x4Ug16lyaaUUKZ/G6QEH1xunKOuXP1F4Vw==
|
||||||
|
|
||||||
google-auth-library@^7.0.0, google-auth-library@^7.6.1, google-auth-library@^7.9.2:
|
google-auth-library@^7.0.0, google-auth-library@^7.6.1, google-auth-library@^7.9.2:
|
||||||
version "7.11.0"
|
version "7.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.11.0.tgz#b63699c65037310a424128a854ba7e736704cbdb"
|
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.11.0.tgz#b63699c65037310a424128a854ba7e736704cbdb"
|
||||||
|
@ -4716,6 +4721,13 @@ react-expanding-textarea@2.3.5:
|
||||||
react-with-forwarded-ref "^0.3.3"
|
react-with-forwarded-ref "^0.3.3"
|
||||||
tslib "^2.0.3"
|
tslib "^2.0.3"
|
||||||
|
|
||||||
|
react-hot-toast@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.2.0.tgz#ab6f4caed4214b9534f94bb8cfaaf21b051e62b9"
|
||||||
|
integrity sha512-248rXw13uhf/6TNDVzagX+y7R8J183rp7MwUMNkcrBRyHj/jWOggfXTGlM8zAOuh701WyVW+eUaWG2LeSufX9g==
|
||||||
|
dependencies:
|
||||||
|
goober "^2.1.1"
|
||||||
|
|
||||||
react-instantsearch-hooks-web@6.24.1:
|
react-instantsearch-hooks-web@6.24.1:
|
||||||
version "6.24.1"
|
version "6.24.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-instantsearch-hooks-web/-/react-instantsearch-hooks-web-6.24.1.tgz#392be70c584583f3cd9fe22eda5a59f7449e5ac9"
|
resolved "https://registry.yarnpkg.com/react-instantsearch-hooks-web/-/react-instantsearch-hooks-web-6.24.1.tgz#392be70c584583f3cd9fe22eda5a59f7449e5ac9"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user