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:
parent
c3d09e5323
commit
33edd3c0fb
|
@ -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!" />
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'}>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user