Create challenge without previous market (#718)

* Create challenge without previous market

* Check if they've balance to create both on fe

* Change wording slightly

* Finish merge
This commit is contained in:
Ian Philips 2022-08-18 08:15:20 -06:00 committed by GitHub
parent c3d09e5323
commit 33edd3c0fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 124 additions and 50 deletions

View File

@ -11,7 +11,7 @@ import { User } from 'common/user'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
import { Button } from '../button' import { Button } from '../button'
import { createChallenge, getChallengeUrl } from 'web/lib/firebase/challenges' import { createChallenge, getChallengeUrl } from 'web/lib/firebase/challenges'
import { BinaryContract } from 'common/contract' import { BinaryContract, MAX_QUESTION_LENGTH } from 'common/contract'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { NoLabel, YesLabel } from '../outcome-label' import { NoLabel, YesLabel } from '../outcome-label'
@ -19,24 +19,32 @@ import { QRCode } from '../qr-code'
import { copyToClipboard } from 'web/lib/util/copy' import { copyToClipboard } from 'web/lib/util/copy'
import { AmountInput } from '../amount-input' import { AmountInput } from '../amount-input'
import { getProbability } from 'common/calculate' import { getProbability } from 'common/calculate'
import { createMarket } from 'web/lib/firebase/api'
import { removeUndefinedProps } from 'common/util/object'
import { FIXED_ANTE } from 'common/antes'
import Textarea from 'react-expanding-textarea'
import { useTextEditor } from 'web/components/editor'
import { LoadingIndicator } from 'web/components/loading-indicator'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
type challengeInfo = { type challengeInfo = {
amount: number amount: number
expiresTime: number | null expiresTime: number | null
message: string
outcome: 'YES' | 'NO' | number outcome: 'YES' | 'NO' | number
acceptorAmount: number acceptorAmount: number
question: string
} }
export function CreateChallengeModal(props: { export function CreateChallengeModal(props: {
user: User | null | undefined user: User | null | undefined
contract: BinaryContract
isOpen: boolean isOpen: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
contract?: BinaryContract
}) { }) {
const { user, contract, isOpen, setOpen } = props const { user, contract, isOpen, setOpen } = props
const [challengeSlug, setChallengeSlug] = useState('') const [challengeSlug, setChallengeSlug] = useState('')
const [loading, setLoading] = useState(false)
const { editor } = useTextEditor({ placeholder: '' })
return ( return (
<Modal open={isOpen} setOpen={setOpen}> <Modal open={isOpen} setOpen={setOpen}>
@ -46,24 +54,42 @@ export function CreateChallengeModal(props: {
<CreateChallengeForm <CreateChallengeForm
user={user} user={user}
contract={contract} contract={contract}
loading={loading}
onCreate={async (newChallenge) => { onCreate={async (newChallenge) => {
const challenge = await createChallenge({ setLoading(true)
creator: user, try {
creatorAmount: newChallenge.amount, const challengeContract = contract
expiresTime: newChallenge.expiresTime, ? contract
message: newChallenge.message, : await createMarket(
acceptorAmount: newChallenge.acceptorAmount, removeUndefinedProps({
outcome: newChallenge.outcome, question: newChallenge.question,
contract: contract, outcomeType: 'BINARY',
}) initialProb: 50,
if (challenge) { description: editor?.getJSON(),
setChallengeSlug(getChallengeUrl(challenge)) ante: FIXED_ANTE,
track('challenge created', { closeTime: dayjs().add(30, 'day').valueOf(),
creator: user.username, })
amount: newChallenge.amount, )
contractId: contract.id, const challenge = await createChallenge({
creator: user,
creatorAmount: newChallenge.amount,
expiresTime: newChallenge.expiresTime,
acceptorAmount: newChallenge.acceptorAmount,
outcome: newChallenge.outcome,
contract: challengeContract as BinaryContract,
}) })
if (challenge) {
setChallengeSlug(getChallengeUrl(challenge))
track('challenge created', {
creator: user.username,
amount: newChallenge.amount,
contractId: challengeContract.id,
})
}
} catch (e) {
console.error("couldn't create market/challenge:", e)
} }
setLoading(false)
}} }}
challengeSlug={challengeSlug} challengeSlug={challengeSlug}
/> />
@ -75,25 +101,24 @@ export function CreateChallengeModal(props: {
function CreateChallengeForm(props: { function CreateChallengeForm(props: {
user: User user: User
contract: BinaryContract
onCreate: (m: challengeInfo) => Promise<void> onCreate: (m: challengeInfo) => Promise<void>
challengeSlug: string challengeSlug: string
loading: boolean
contract?: BinaryContract
}) { }) {
const { user, onCreate, contract, challengeSlug } = props const { user, onCreate, contract, challengeSlug, loading } = props
const [isCreating, setIsCreating] = useState(false) const [isCreating, setIsCreating] = useState(false)
const [finishedCreating, setFinishedCreating] = useState(false) const [finishedCreating, setFinishedCreating] = useState(false)
const [error, setError] = useState<string>('') const [error, setError] = useState<string>('')
const [editingAcceptorAmount, setEditingAcceptorAmount] = useState(false) const [editingAcceptorAmount, setEditingAcceptorAmount] = useState(false)
const defaultExpire = 'week' const defaultExpire = 'week'
const defaultMessage = `${user.name} is challenging you to a bet! Do you think ${contract.question}`
const [challengeInfo, setChallengeInfo] = useState<challengeInfo>({ const [challengeInfo, setChallengeInfo] = useState<challengeInfo>({
expiresTime: dayjs().add(2, defaultExpire).valueOf(), expiresTime: dayjs().add(2, defaultExpire).valueOf(),
outcome: 'YES', outcome: 'YES',
amount: 100, amount: 100,
acceptorAmount: 100, acceptorAmount: 100,
message: defaultMessage, question: contract ? contract.question : '',
}) })
useEffect(() => { useEffect(() => {
setError('') setError('')
@ -106,7 +131,15 @@ function CreateChallengeForm(props: {
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault()
if (user.balance < challengeInfo.amount) { if (user.balance < challengeInfo.amount) {
setError('You do not have enough mana to create this challenge') setError("You don't have enough mana to create this challenge")
return
}
if (!contract && user.balance < FIXED_ANTE + challengeInfo.amount) {
setError(
`You don't have enough mana to create this challenge and market. You need ${formatMoney(
FIXED_ANTE + challengeInfo.amount
)}`
)
return return
} }
setIsCreating(true) setIsCreating(true)
@ -118,7 +151,23 @@ function CreateChallengeForm(props: {
<div className="mb-8"> <div className="mb-8">
Challenge a friend to bet on{' '} Challenge a friend to bet on{' '}
<span className="underline">{contract.question}</span> {contract ? (
<span className="underline">{contract.question}</span>
) : (
<Textarea
placeholder="e.g. Will a Democrat be the next president?"
className="input input-bordered mt-1 w-full resize-none"
autoFocus={true}
maxLength={MAX_QUESTION_LENGTH}
value={challengeInfo.question}
onChange={(e) =>
setChallengeInfo({
...challengeInfo,
question: e.target.value,
})
}
/>
)}
</div> </div>
<div className="mt-2 flex flex-col flex-wrap justify-center gap-x-5 gap-y-2"> <div className="mt-2 flex flex-col flex-wrap justify-center gap-x-5 gap-y-2">
@ -187,22 +236,23 @@ function CreateChallengeForm(props: {
{challengeInfo.outcome === 'YES' ? <NoLabel /> : <YesLabel />} {challengeInfo.outcome === 'YES' ? <NoLabel /> : <YesLabel />}
</Row> </Row>
</div> </div>
<Button {contract && (
size="2xs" <Button
color="gray" size="2xs"
onClick={() => { color="gray"
setEditingAcceptorAmount(true) onClick={() => {
setEditingAcceptorAmount(true)
const p = getProbability(contract)
const prob = challengeInfo.outcome === 'YES' ? p : 1 - p
const { amount } = challengeInfo
const acceptorAmount = Math.round(amount / prob - amount)
setChallengeInfo({ ...challengeInfo, acceptorAmount })
}}
>
Use market odds
</Button>
const p = getProbability(contract)
const prob = challengeInfo.outcome === 'YES' ? p : 1 - p
const { amount } = challengeInfo
const acceptorAmount = Math.round(amount / prob - amount)
setChallengeInfo({ ...challengeInfo, acceptorAmount })
}}
>
Use market odds
</Button>
)}
<div className="mt-8"> <div className="mt-8">
If the challenge is accepted, whoever is right will earn{' '} If the challenge is accepted, whoever is right will earn{' '}
<span className="font-semibold"> <span className="font-semibold">
@ -210,7 +260,18 @@ function CreateChallengeForm(props: {
challengeInfo.acceptorAmount + challengeInfo.amount || 0 challengeInfo.acceptorAmount + challengeInfo.amount || 0
)} )}
</span>{' '} </span>{' '}
in total. in total.{' '}
<span>
{!contract && (
<span>
Because there's no market yet, you'll be charged
<span className={'mx-1 font-semibold'}>
{formatMoney(FIXED_ANTE)}
</span>
to create it.
</span>
)}
</span>
</div> </div>
<Row className="mt-8 items-center"> <Row className="mt-8 items-center">
@ -218,10 +279,8 @@ function CreateChallengeForm(props: {
type="submit" type="submit"
color={'gradient'} color={'gradient'}
size="xl" size="xl"
className={clsx( disabled={isCreating || challengeInfo.question === ''}
'whitespace-nowrap drop-shadow-md', className={clsx('whitespace-nowrap drop-shadow-md')}
isCreating ? 'disabled' : ''
)}
> >
Create challenge bet Create challenge bet
</Button> </Button>
@ -229,7 +288,12 @@ function CreateChallengeForm(props: {
<Row className={'text-error'}>{error} </Row> <Row className={'text-error'}>{error} </Row>
</form> </form>
)} )}
{finishedCreating && ( {loading && (
<Col className={'h-56 w-full items-center justify-center'}>
<LoadingIndicator />
</Col>
)}
{finishedCreating && !loading && (
<> <>
<Title className="!my-0" text="Challenge Created!" /> <Title className="!my-0" text="Challenge Created!" />

View File

@ -29,13 +29,11 @@ export async function createChallenge(data: {
creatorAmount: number creatorAmount: number
acceptorAmount: number acceptorAmount: number
expiresTime: number | null expiresTime: number | null
message: string
}) { }) {
const { const {
creator, creator,
creatorAmount, creatorAmount,
expiresTime, expiresTime,
message,
contract, contract,
outcome, outcome,
acceptorAmount, acceptorAmount,
@ -73,7 +71,7 @@ export async function createChallenge(data: {
acceptedByUserIds: [], acceptedByUserIds: [],
acceptances: [], acceptances: [],
isResolved: false, isResolved: false,
message, message: '',
} }
await setDoc(doc(challenges(contract.id), slug), challenge) await setDoc(doc(challenges(contract.id), slug), challenge)

View File

@ -16,7 +16,7 @@ import {
useAcceptedChallenges, useAcceptedChallenges,
useUserChallenges, useUserChallenges,
} from 'web/lib/firebase/challenges' } from 'web/lib/firebase/challenges'
import { Challenge } from 'common/challenge' import { Challenge, CHALLENGES_ENABLED } from 'common/challenge'
import { Tabs } from 'web/components/layout/tabs' import { Tabs } from 'web/components/layout/tabs'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { UserLink } from 'web/components/user-page' import { UserLink } from 'web/components/user-page'
@ -29,6 +29,7 @@ import { copyToClipboard } from 'web/lib/util/copy'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
import { QRCode } from 'web/components/qr-code' import { QRCode } from 'web/components/qr-code'
import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal'
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const columnClass = 'sm:px-5 px-2 py-3.5 max-w-[100px] truncate' const columnClass = 'sm:px-5 px-2 py-3.5 max-w-[100px] truncate'
@ -37,6 +38,7 @@ const amountClass = columnClass + ' max-w-[75px] font-bold'
export default function ChallengesListPage() { export default function ChallengesListPage() {
const user = useUser() const user = useUser()
const challenges = useAcceptedChallenges() const challenges = useAcceptedChallenges()
const [open, setOpen] = React.useState(false)
const userChallenges = useUserChallenges(user?.id) const userChallenges = useUserChallenges(user?.id)
.concat( .concat(
user ? challenges.filter((c) => c.acceptances[0].userId === user.id) : [] user ? challenges.filter((c) => c.acceptances[0].userId === user.id) : []
@ -70,6 +72,16 @@ export default function ChallengesListPage() {
<Col className="w-full px-8"> <Col className="w-full px-8">
<Row className="items-center justify-between"> <Row className="items-center justify-between">
<Title text="Challenges" /> <Title text="Challenges" />
{CHALLENGES_ENABLED && (
<Button size="lg" color="gradient" onClick={() => setOpen(true)}>
Create Challenge
<CreateChallengeModal
isOpen={open}
setOpen={setOpen}
user={user}
/>
</Button>
)}
</Row> </Row>
<p> <p>
<SiteLink className={'font-bold'} href={'/home'}> <SiteLink className={'font-bold'} href={'/home'}>