Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2a134b3c9a | ||
|
81f69fde21 | ||
|
e3e08f449c | ||
|
97c0a5cfa8 | ||
|
f73b13644f | ||
|
7e177c0ebf | ||
|
00717c90ef | ||
|
49f60cdb80 | ||
|
5690444135 | ||
|
2359b5b337 | ||
|
eec4e87da0 | ||
|
ac76094174 | ||
|
977268e9fc |
|
@ -1,3 +1,4 @@
|
||||||
|
import { JSONContent } from '@tiptap/core'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
|
|
||||||
export type Answer = {
|
export type Answer = {
|
||||||
|
@ -12,6 +13,7 @@ export type Answer = {
|
||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
|
|
||||||
text: string
|
text: string
|
||||||
|
description?: string | JSONContent
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNoneAnswer = (contractId: string, creator: User) => {
|
export const getNoneAnswer = (contractId: string, creator: User) => {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
PseudoNumericContract,
|
PseudoNumericContract,
|
||||||
MultipleChoiceContract,
|
MultipleChoiceContract,
|
||||||
|
AnswerContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { floatingEqual } from './util/math'
|
import { floatingEqual } from './util/math'
|
||||||
|
|
||||||
|
@ -201,9 +202,7 @@ export function getContractBetNullMetrics() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTopAnswer(
|
export function getTopAnswer(contract: AnswerContract) {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
|
||||||
) {
|
|
||||||
const { answers } = contract
|
const { answers } = contract
|
||||||
const top = maxBy(
|
const top = maxBy(
|
||||||
answers?.map((answer) => ({
|
answers?.map((answer) => ({
|
||||||
|
|
|
@ -10,6 +10,7 @@ export type AnyOutcomeType =
|
||||||
| PseudoNumeric
|
| PseudoNumeric
|
||||||
| FreeResponse
|
| FreeResponse
|
||||||
| Numeric
|
| Numeric
|
||||||
|
| Bounty
|
||||||
export type AnyContractType =
|
export type AnyContractType =
|
||||||
| (CPMM & Binary)
|
| (CPMM & Binary)
|
||||||
| (CPMM & PseudoNumeric)
|
| (CPMM & PseudoNumeric)
|
||||||
|
@ -17,6 +18,7 @@ export type AnyContractType =
|
||||||
| (DPM & FreeResponse)
|
| (DPM & FreeResponse)
|
||||||
| (DPM & Numeric)
|
| (DPM & Numeric)
|
||||||
| (DPM & MultipleChoice)
|
| (DPM & MultipleChoice)
|
||||||
|
| (DPM & Bounty)
|
||||||
|
|
||||||
export type Contract<T extends AnyContractType = AnyContractType> = {
|
export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
id: string
|
id: string
|
||||||
|
@ -64,10 +66,15 @@ export type PseudoNumericContract = Contract & PseudoNumeric
|
||||||
export type NumericContract = Contract & Numeric
|
export type NumericContract = Contract & Numeric
|
||||||
export type FreeResponseContract = Contract & FreeResponse
|
export type FreeResponseContract = Contract & FreeResponse
|
||||||
export type MultipleChoiceContract = Contract & MultipleChoice
|
export type MultipleChoiceContract = Contract & MultipleChoice
|
||||||
|
export type BountyContract = Contract & Bounty
|
||||||
export type DPMContract = Contract & DPM
|
export type DPMContract = Contract & DPM
|
||||||
export type CPMMContract = Contract & CPMM
|
export type CPMMContract = Contract & CPMM
|
||||||
export type DPMBinaryContract = BinaryContract & DPM
|
export type DPMBinaryContract = BinaryContract & DPM
|
||||||
export type CPMMBinaryContract = BinaryContract & CPMM
|
export type CPMMBinaryContract = BinaryContract & CPMM
|
||||||
|
export type AnswerContract =
|
||||||
|
| FreeResponseContract
|
||||||
|
| MultipleChoiceContract
|
||||||
|
| BountyContract
|
||||||
|
|
||||||
export type DPM = {
|
export type DPM = {
|
||||||
mechanism: 'dpm-2'
|
mechanism: 'dpm-2'
|
||||||
|
@ -127,6 +134,21 @@ export type Numeric = {
|
||||||
resolutionValue?: number
|
resolutionValue?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Bounty = {
|
||||||
|
outcomeType: 'BOUNTY'
|
||||||
|
// One answer for each submission
|
||||||
|
answers: Answer[]
|
||||||
|
// Resolution = which answer id the bounty went to
|
||||||
|
resolution?: string | 'MKT' | 'CANCEL'
|
||||||
|
resolutions?: { [outcome: string]: number } // Used for MKT resolution
|
||||||
|
|
||||||
|
// Amount contributed to each bounty, and by who
|
||||||
|
prizes: {
|
||||||
|
[giverId: string]: number
|
||||||
|
}
|
||||||
|
totalPrizes: number
|
||||||
|
}
|
||||||
|
|
||||||
export type outcomeType = AnyOutcomeType['outcomeType']
|
export type outcomeType = AnyOutcomeType['outcomeType']
|
||||||
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||||
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
||||||
|
@ -136,6 +158,7 @@ export const OUTCOME_TYPES = [
|
||||||
'FREE_RESPONSE',
|
'FREE_RESPONSE',
|
||||||
'PSEUDO_NUMERIC',
|
'PSEUDO_NUMERIC',
|
||||||
'NUMERIC',
|
'NUMERIC',
|
||||||
|
'BOUNTY',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const MAX_QUESTION_LENGTH = 480
|
export const MAX_QUESTION_LENGTH = 480
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
getCpmmProbability,
|
getCpmmProbability,
|
||||||
} from './calculate-cpmm'
|
} from './calculate-cpmm'
|
||||||
import {
|
import {
|
||||||
|
AnswerContract,
|
||||||
CPMMBinaryContract,
|
CPMMBinaryContract,
|
||||||
DPMBinaryContract,
|
DPMBinaryContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
|
@ -323,7 +324,7 @@ export const getNewBinaryDpmBetInfo = (
|
||||||
export const getNewMultiBetInfo = (
|
export const getNewMultiBetInfo = (
|
||||||
outcome: string,
|
outcome: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: FreeResponseContract | MultipleChoiceContract,
|
contract: AnswerContract,
|
||||||
loanAmount: number
|
loanAmount: number
|
||||||
) => {
|
) => {
|
||||||
const { pool, totalShares, totalBets } = contract
|
const { pool, totalShares, totalBets } = contract
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { range } from 'lodash'
|
import { range } from 'lodash'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
|
Bounty,
|
||||||
Contract,
|
Contract,
|
||||||
CPMM,
|
CPMM,
|
||||||
DPM,
|
DPM,
|
||||||
|
@ -45,16 +46,22 @@ export function getNewContract(
|
||||||
)
|
)
|
||||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||||
|
|
||||||
const propsByOutcomeType =
|
const PROPS = {
|
||||||
outcomeType === 'BINARY'
|
BINARY: getBinaryCpmmProps(initialProb, ante), // getBinaryDpmProps(initialProb, ante)
|
||||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
PSEUDO_NUMERIC: getPseudoNumericCpmmProps(
|
||||||
: outcomeType === 'PSEUDO_NUMERIC'
|
initialProb,
|
||||||
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
|
ante,
|
||||||
: outcomeType === 'NUMERIC'
|
min,
|
||||||
? getNumericProps(ante, bucketCount, min, max)
|
max,
|
||||||
: outcomeType === 'MULTIPLE_CHOICE'
|
isLogScale
|
||||||
? getMultipleChoiceProps(ante, answers)
|
),
|
||||||
: getFreeAnswerProps(ante)
|
FREE_RESPONSE: getFreeAnswerProps(ante),
|
||||||
|
MULTIPLE_CHOICE: getMultipleChoiceProps(ante, answers),
|
||||||
|
NUMERIC: getNumericProps(ante, bucketCount, min, max),
|
||||||
|
BOUNTY: getBountyProps(ante, creator),
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsByOutcomeType = PROPS[outcomeType] || PROPS['FREE_RESPONSE']
|
||||||
|
|
||||||
const contract: Contract = removeUndefinedProps({
|
const contract: Contract = removeUndefinedProps({
|
||||||
id,
|
id,
|
||||||
|
@ -157,6 +164,18 @@ const getFreeAnswerProps = (ante: number) => {
|
||||||
return system
|
return system
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBountyProps(ante: number, creator: User) {
|
||||||
|
const system: DPM & Bounty = {
|
||||||
|
...getFreeAnswerProps(ante),
|
||||||
|
outcomeType: 'BOUNTY',
|
||||||
|
prizes: {
|
||||||
|
[creator.id]: ante,
|
||||||
|
},
|
||||||
|
totalPrizes: ante,
|
||||||
|
}
|
||||||
|
return system
|
||||||
|
}
|
||||||
|
|
||||||
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
|
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
|
||||||
const numAnswers = answers.length
|
const numAnswers = answers.length
|
||||||
const betAnte = ante / numAnswers
|
const betAnte = ante / numAnswers
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
|
||||||
import { Bet, NumericBet } from './bet'
|
import { Bet, NumericBet } from './bet'
|
||||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
||||||
import {
|
import {
|
||||||
|
AnswerContract,
|
||||||
DPMContract,
|
DPMContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
MultipleChoiceContract,
|
MultipleChoiceContract,
|
||||||
|
@ -184,7 +185,7 @@ export const getDpmMktPayouts = (
|
||||||
|
|
||||||
export const getPayoutsMultiOutcome = (
|
export const getPayoutsMultiOutcome = (
|
||||||
resolutions: { [outcome: string]: number },
|
resolutions: { [outcome: string]: number },
|
||||||
contract: FreeResponseContract | MultipleChoiceContract,
|
contract: AnswerContract,
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
) => {
|
) => {
|
||||||
const poolTotal = sum(Object.values(contract.pool))
|
const poolTotal = sum(Object.values(contract.pool))
|
||||||
|
|
|
@ -7,5 +7,11 @@
|
||||||
"firestore": {
|
"firestore": {
|
||||||
"rules": "firestore.rules",
|
"rules": "firestore.rules",
|
||||||
"indexes": "firestore.indexes.json"
|
"indexes": "firestore.indexes.json"
|
||||||
|
},
|
||||||
|
"emulators": {
|
||||||
|
"firestore": {
|
||||||
|
"port": 8080,
|
||||||
|
"host": "127.0.0.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
55
functions/src/add-bounty.ts
Normal file
55
functions/src/add-bounty.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
const bodySchema = z.object({
|
||||||
|
contractId: z.string(),
|
||||||
|
amount: z.number().gt(0),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addbounty = newEndpoint({}, async (req, auth) => {
|
||||||
|
const { amount, contractId } = validate(bodySchema, req.body)
|
||||||
|
|
||||||
|
return await firestore.runTransaction(async (transaction) => {
|
||||||
|
const userDoc = firestore.doc(`users/${auth.uid}`)
|
||||||
|
const userSnap = await transaction.get(userDoc)
|
||||||
|
if (!userSnap.exists) throw new APIError(400, 'User not found')
|
||||||
|
const user = userSnap.data() as User
|
||||||
|
|
||||||
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
|
const contractSnap = await transaction.get(contractDoc)
|
||||||
|
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
||||||
|
const contract = contractSnap.data() as Contract
|
||||||
|
if (contract.outcomeType !== 'BOUNTY')
|
||||||
|
throw new APIError(400, "Can't add bounties to non-BOUNTY contracts")
|
||||||
|
|
||||||
|
const { closeTime } = contract
|
||||||
|
if (closeTime && Date.now() > closeTime)
|
||||||
|
throw new APIError(400, 'Contract is closed')
|
||||||
|
|
||||||
|
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
|
||||||
|
|
||||||
|
// TODO: Capture with txns?
|
||||||
|
transaction.update(userDoc, {
|
||||||
|
balance: user.balance - amount,
|
||||||
|
totalDeposits: user.totalDeposits - amount,
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingPrize = contract.prizes[user.id] ?? 0
|
||||||
|
|
||||||
|
transaction.update(contractDoc, {
|
||||||
|
prizes: {
|
||||||
|
...contract.prizes,
|
||||||
|
[user.id]: existingPrize + amount,
|
||||||
|
},
|
||||||
|
totalPrizes: contract.totalPrizes + amount,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
})
|
||||||
|
})
|
|
@ -36,7 +36,10 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
|
||||||
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
|
|
||||||
if (contract.outcomeType !== 'FREE_RESPONSE')
|
if (
|
||||||
|
contract.outcomeType !== 'FREE_RESPONSE' &&
|
||||||
|
contract.outcomeType !== 'BOUNTY'
|
||||||
|
)
|
||||||
throw new APIError(400, 'Requires a free response contract')
|
throw new APIError(400, 'Requires a free response contract')
|
||||||
|
|
||||||
const { closeTime, volume } = contract
|
const { closeTime, volume } = contract
|
||||||
|
|
|
@ -234,7 +234,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await contractRef.update({ answers: answerObjects })
|
await contractRef.update({ answers: answerObjects })
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
} else if (outcomeType === 'FREE_RESPONSE' || outcomeType === 'BOUNTY') {
|
||||||
const noneAnswerDoc = firestore
|
const noneAnswerDoc = firestore
|
||||||
.collection(`contracts/${contract.id}/answers`)
|
.collection(`contracts/${contract.id}/answers`)
|
||||||
.doc('0')
|
.doc('0')
|
||||||
|
|
|
@ -66,6 +66,7 @@ import { stripewebhook, createcheckoutsession } from './stripe'
|
||||||
import { getcurrentuser } from './get-current-user'
|
import { getcurrentuser } from './get-current-user'
|
||||||
import { acceptchallenge } from './accept-challenge'
|
import { acceptchallenge } from './accept-challenge'
|
||||||
import { getcustomtoken } from './get-custom-token'
|
import { getcustomtoken } from './get-custom-token'
|
||||||
|
import { addbounty } from './add-bounty'
|
||||||
|
|
||||||
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
||||||
return onRequest(opts, handler as any)
|
return onRequest(opts, handler as any)
|
||||||
|
@ -91,6 +92,7 @@ const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
|
||||||
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
||||||
const acceptChallenge = toCloudFunction(acceptchallenge)
|
const acceptChallenge = toCloudFunction(acceptchallenge)
|
||||||
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
||||||
|
const addBountyFunction = toCloudFunction(addbounty)
|
||||||
|
|
||||||
export {
|
export {
|
||||||
healthFunction as health,
|
healthFunction as health,
|
||||||
|
@ -114,4 +116,5 @@ export {
|
||||||
getCurrentUserFunction as getcurrentuser,
|
getCurrentUserFunction as getcurrentuser,
|
||||||
acceptChallenge as acceptchallenge,
|
acceptChallenge as acceptchallenge,
|
||||||
getCustomTokenFunction as getcustomtoken,
|
getCustomTokenFunction as getcustomtoken,
|
||||||
|
addBountyFunction as addbounty,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
||||||
|
|
||||||
import {
|
import { AnswerContract, Contract, RESOLUTIONS } from '../../common/contract'
|
||||||
Contract,
|
|
||||||
FreeResponseContract,
|
|
||||||
MultipleChoiceContract,
|
|
||||||
RESOLUTIONS,
|
|
||||||
} from '../../common/contract'
|
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getUser, isProd, payUser } from './utils'
|
import { getUser, isProd, payUser } from './utils'
|
||||||
|
@ -295,10 +290,7 @@ function getResolutionParams(contract: Contract, body: string) {
|
||||||
throw new APIError(500, `Invalid outcome type: ${outcomeType}`)
|
throw new APIError(500, `Invalid outcome type: ${outcomeType}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateAnswer(
|
function validateAnswer(contract: AnswerContract, answer: number) {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract,
|
|
||||||
answer: number
|
|
||||||
) {
|
|
||||||
const validIds = contract.answers.map((a) => a.id)
|
const validIds = contract.answers.map((a) => a.id)
|
||||||
if (!validIds.includes(answer.toString())) {
|
if (!validIds.includes(answer.toString())) {
|
||||||
throw new APIError(400, `${answer} is not a valid answer ID`)
|
throw new APIError(400, `${answer} is not a valid answer ID`)
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { unsubscribe } from './unsubscribe'
|
||||||
import { stripewebhook, createcheckoutsession } from './stripe'
|
import { stripewebhook, createcheckoutsession } from './stripe'
|
||||||
import { getcurrentuser } from './get-current-user'
|
import { getcurrentuser } from './get-current-user'
|
||||||
import { getcustomtoken } from './get-custom-token'
|
import { getcustomtoken } from './get-custom-token'
|
||||||
|
import { addbounty } from './add-bounty'
|
||||||
|
|
||||||
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
||||||
const app = express()
|
const app = express()
|
||||||
|
@ -65,6 +66,7 @@ addJsonEndpointRoute('/resolvemarket', resolvemarket)
|
||||||
addJsonEndpointRoute('/unsubscribe', unsubscribe)
|
addJsonEndpointRoute('/unsubscribe', unsubscribe)
|
||||||
addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession)
|
addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession)
|
||||||
addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
|
addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
|
||||||
|
addJsonEndpointRoute('/addbounty', addbounty)
|
||||||
addEndpointRoute('/getcustomtoken', getcustomtoken)
|
addEndpointRoute('/getcustomtoken', getcustomtoken)
|
||||||
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ To run the development server, install [Yarn 1.x][yarn], and then in this direct
|
||||||
3. Your site will be available on http://localhost:3000
|
3. Your site will be available on http://localhost:3000
|
||||||
|
|
||||||
Check package.json for other command-line tasks. (e.g. `yarn dev` will point the development server at the prod
|
Check package.json for other command-line tasks. (e.g. `yarn dev` will point the development server at the prod
|
||||||
database. `yarn emulate` will run against a local emulated database, if you are serving it via `yarn serve` from the
|
database. `yarn dev:emulate` will run against a local emulated database, if you are serving it via `yarn serve` from the
|
||||||
[`functions/` package][functions-readme].)
|
[`functions/` package][functions-readme].)
|
||||||
|
|
||||||
## Tech stack
|
## Tech stack
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||||
import { XIcon } from '@heroicons/react/solid'
|
import { XIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
import { AnswerContract } from 'common/contract'
|
||||||
import { BuyAmountInput } from '../amount-input'
|
import { BuyAmountInput } from '../amount-input'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { APIError, placeBet } from 'web/lib/firebase/api'
|
import { APIError, placeBet } from 'web/lib/firebase/api'
|
||||||
|
@ -30,7 +30,7 @@ import { AlertBox } from '../alert-box'
|
||||||
|
|
||||||
export function AnswerBetPanel(props: {
|
export function AnswerBetPanel(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: AnswerContract
|
||||||
closePanel: () => void
|
closePanel: () => void
|
||||||
className?: string
|
className?: string
|
||||||
isModal?: boolean
|
isModal?: boolean
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
import { AnswerContract } from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
|
@ -13,7 +13,7 @@ import { Linkify } from '../linkify'
|
||||||
|
|
||||||
export function AnswerItem(props: {
|
export function AnswerItem(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: AnswerContract
|
||||||
showChoice: 'radio' | 'checkbox' | undefined
|
showChoice: 'radio' | 'checkbox' | undefined
|
||||||
chosenProb: number | undefined
|
chosenProb: number | undefined
|
||||||
totalChosenProb?: number
|
totalChosenProb?: number
|
||||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx'
|
||||||
import { sum } from 'lodash'
|
import { sum } from 'lodash'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
import { AnswerContract } from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { APIError, resolveMarket } from 'web/lib/firebase/api'
|
import { APIError, resolveMarket } from 'web/lib/firebase/api'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
@ -11,7 +11,7 @@ import { ResolveConfirmationButton } from '../confirmation-button'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
|
|
||||||
export function AnswerResolvePanel(props: {
|
export function AnswerResolvePanel(props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: AnswerContract
|
||||||
resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||||
setResolveOption: (
|
setResolveOption: (
|
||||||
option: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
option: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||||
|
|
|
@ -5,14 +5,18 @@ import { groupBy, sortBy, sumBy } from 'lodash'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
import {
|
||||||
|
AnswerContract,
|
||||||
|
FreeResponseContract,
|
||||||
|
MultipleChoiceContract,
|
||||||
|
} from 'common/contract'
|
||||||
import { getOutcomeProbability } from 'common/calculate'
|
import { getOutcomeProbability } from 'common/calculate'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
|
||||||
const NUM_LINES = 6
|
const NUM_LINES = 6
|
||||||
|
|
||||||
export const AnswersGraph = memo(function AnswersGraph(props: {
|
export const AnswersGraph = memo(function AnswersGraph(props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: AnswerContract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
height?: number
|
height?: number
|
||||||
}) {
|
}) {
|
||||||
|
@ -178,10 +182,7 @@ function formatTime(
|
||||||
return d.format(format)
|
return d.format(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeProbsByOutcome = (
|
const computeProbsByOutcome = (bets: Bet[], contract: AnswerContract) => {
|
||||||
bets: Bet[],
|
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
|
||||||
) => {
|
|
||||||
const { totalBets, outcomeType } = contract
|
const { totalBets, outcomeType } = contract
|
||||||
|
|
||||||
const betsByOutcome = groupBy(bets, (bet) => bet.outcome)
|
const betsByOutcome = groupBy(bets, (bet) => bet.outcome)
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { sortBy, partition, sum, uniq } from 'lodash'
|
import { sortBy, partition, sum, uniq } from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
import {
|
||||||
|
AnswerContract,
|
||||||
|
BountyContract,
|
||||||
|
FreeResponseContract,
|
||||||
|
MultipleChoiceContract,
|
||||||
|
} from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
||||||
|
@ -25,9 +30,7 @@ import { UserLink } from 'web/components/user-page'
|
||||||
import { Linkify } from 'web/components/linkify'
|
import { Linkify } from 'web/components/linkify'
|
||||||
import { BuyButton } from 'web/components/yes-no-selector'
|
import { BuyButton } from 'web/components/yes-no-selector'
|
||||||
|
|
||||||
export function AnswersPanel(props: {
|
export function AnswersPanel(props: { contract: AnswerContract }) {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
|
||||||
}) {
|
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
const { creatorId, resolution, resolutions, totalBets, outcomeType } =
|
const { creatorId, resolution, resolutions, totalBets, outcomeType } =
|
||||||
contract
|
contract
|
||||||
|
@ -136,7 +139,7 @@ export function AnswersPanel(props: {
|
||||||
<div className="pb-4 text-gray-500">No answers yet...</div>
|
<div className="pb-4 text-gray-500">No answers yet...</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{outcomeType === 'FREE_RESPONSE' &&
|
{(outcomeType === 'FREE_RESPONSE' || outcomeType === 'BOUNTY') &&
|
||||||
tradingAllowed(contract) &&
|
tradingAllowed(contract) &&
|
||||||
(!resolveOption || resolveOption === 'CANCEL') && (
|
(!resolveOption || resolveOption === 'CANCEL') && (
|
||||||
<CreateAnswerPanel contract={contract} />
|
<CreateAnswerPanel contract={contract} />
|
||||||
|
@ -158,7 +161,7 @@ export function AnswersPanel(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAnswerItems(
|
function getAnswerItems(
|
||||||
contract: FreeResponseContract | MultipleChoiceContract,
|
contract: AnswerContract,
|
||||||
answers: Answer[],
|
answers: Answer[],
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
) {
|
) {
|
||||||
|
@ -184,7 +187,7 @@ function getAnswerItems(
|
||||||
}
|
}
|
||||||
|
|
||||||
function OpenAnswer(props: {
|
function OpenAnswer(props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: AnswerContract
|
||||||
answer: Answer
|
answer: Answer
|
||||||
items: ActivityItem[]
|
items: ActivityItem[]
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useState } from 'react'
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
import { findBestMatch } from 'string-similarity'
|
import { findBestMatch } from 'string-similarity'
|
||||||
|
|
||||||
import { FreeResponseContract } from 'common/contract'
|
import { BountyContract, FreeResponseContract } from 'common/contract'
|
||||||
import { BuyAmountInput } from '../amount-input'
|
import { BuyAmountInput } from '../amount-input'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { APIError, createAnswer } from 'web/lib/firebase/api'
|
import { APIError, createAnswer } from 'web/lib/firebase/api'
|
||||||
|
@ -26,7 +26,9 @@ import { MAX_ANSWER_LENGTH } from 'common/answer'
|
||||||
import { withTracking } from 'web/lib/service/analytics'
|
import { withTracking } from 'web/lib/service/analytics'
|
||||||
import { lowerCase } from 'lodash'
|
import { lowerCase } from 'lodash'
|
||||||
|
|
||||||
export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
export function CreateAnswerPanel(props: {
|
||||||
|
contract: FreeResponseContract | BountyContract
|
||||||
|
}) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [text, setText] = useState('')
|
const [text, setText] = useState('')
|
||||||
|
|
73
web/components/bounty/bounty-box.tsx
Normal file
73
web/components/bounty/bounty-box.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { BountyContract } from 'common/contract'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { addBounty } from 'web/lib/firebase/api'
|
||||||
|
import { BuyAmountInput } from '../amount-input'
|
||||||
|
import { Spacer } from '../layout/spacer'
|
||||||
|
import { Title } from '../title'
|
||||||
|
|
||||||
|
export function BountyBox(props: {
|
||||||
|
className?: string
|
||||||
|
user?: User | null
|
||||||
|
contract: BountyContract
|
||||||
|
}) {
|
||||||
|
const { className, user, contract } = props
|
||||||
|
const [amount, setAmount] = useState<number | undefined>()
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState<string | undefined>()
|
||||||
|
|
||||||
|
const donateDisabled = isSubmitting || !amount || error
|
||||||
|
|
||||||
|
const onSubmit: React.FormEventHandler = async (e) => {
|
||||||
|
if (!user || donateDisabled) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
setIsSubmitting(true)
|
||||||
|
setError(undefined)
|
||||||
|
|
||||||
|
await addBounty({
|
||||||
|
amount,
|
||||||
|
contractId: contract.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsSubmitting(false)
|
||||||
|
setAmount(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(className, 'rounded-lg bg-white py-6 px-8 shadow-lg')}>
|
||||||
|
<Title text="Add to bounty" className="!mt-0" />
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<label
|
||||||
|
className="mb-2 block text-sm text-gray-500"
|
||||||
|
htmlFor="donate-input"
|
||||||
|
>
|
||||||
|
Contribute
|
||||||
|
</label>
|
||||||
|
<BuyAmountInput
|
||||||
|
inputClassName="w-full max-w-none donate-input"
|
||||||
|
amount={amount}
|
||||||
|
onChange={setAmount}
|
||||||
|
error={error}
|
||||||
|
setError={setError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Spacer h={8} />
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
'btn w-full',
|
||||||
|
donateDisabled ? 'btn-disabled' : 'btn-primary',
|
||||||
|
isSubmitting && 'loading'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Add to bounty
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,11 +1,17 @@
|
||||||
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 { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import {
|
import {
|
||||||
|
AnswerContract,
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
|
BountyContract,
|
||||||
Contract,
|
Contract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
MultipleChoiceContract,
|
MultipleChoiceContract,
|
||||||
|
@ -169,6 +175,13 @@ export function ContractCard(props: {
|
||||||
truncate="long"
|
truncate="long"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{outcomeType === 'BOUNTY' && (
|
||||||
|
<BountyValue
|
||||||
|
className="items-center self-center pr-5"
|
||||||
|
contract={contract}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ProbBar contract={contract} />
|
<ProbBar contract={contract} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -211,8 +224,25 @@ export function BinaryResolutionOrChance(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function BountyValue(props: {
|
||||||
|
contract: BountyContract
|
||||||
|
large?: boolean
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, large, className } = props
|
||||||
|
const textColor = `text-${getColor(contract)}`
|
||||||
|
return (
|
||||||
|
<Col className={clsx(large ? 'text-3xl' : 'text-2xl', className)}>
|
||||||
|
<div className={textColor}>{formatMoney(contract.totalPrizes)}</div>
|
||||||
|
<div className={clsx(textColor, large ? 'text-xl' : 'text-base')}>
|
||||||
|
bounty
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function FreeResponseTopAnswer(props: {
|
function FreeResponseTopAnswer(props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: AnswerContract
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -32,14 +32,15 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
'userId'
|
'userId'
|
||||||
).length
|
).length
|
||||||
|
|
||||||
const typeDisplay =
|
const TYPES = {
|
||||||
outcomeType === 'BINARY'
|
BINARY: 'YES / NO',
|
||||||
? 'YES / NO'
|
FREE_RESPONSE: 'Free Response',
|
||||||
: outcomeType === 'FREE_RESPONSE'
|
MULTIPLE_CHOICE: 'Multiple Choice',
|
||||||
? 'Free response'
|
NUMERIC: 'Numeric (deprecated)',
|
||||||
: outcomeType === 'MULTIPLE_CHOICE'
|
PSEUDO_NUMERIC: 'Numeric',
|
||||||
? 'Multiple choice'
|
BOUNTY: 'Bounty',
|
||||||
: 'Numeric'
|
}
|
||||||
|
const typeDisplay = TYPES[outcomeType] || 'Unknown'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Row } from '../layout/row'
|
||||||
import { Linkify } from '../linkify'
|
import { Linkify } from '../linkify'
|
||||||
import {
|
import {
|
||||||
BinaryResolutionOrChance,
|
BinaryResolutionOrChance,
|
||||||
|
BountyValue,
|
||||||
FreeResponseResolutionOrChance,
|
FreeResponseResolutionOrChance,
|
||||||
NumericResolutionOrExpectation,
|
NumericResolutionOrExpectation,
|
||||||
PseudoNumericResolutionOrExpectation,
|
PseudoNumericResolutionOrExpectation,
|
||||||
|
@ -44,7 +45,6 @@ export const ContractOverview = (props: {
|
||||||
<div className="text-2xl text-indigo-700 md:text-3xl">
|
<div className="text-2xl text-indigo-700 md:text-3xl">
|
||||||
<Linkify text={question} />
|
<Linkify text={question} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isBinary && (
|
{isBinary && (
|
||||||
<BinaryResolutionOrChance
|
<BinaryResolutionOrChance
|
||||||
className="hidden items-end xl:flex"
|
className="hidden items-end xl:flex"
|
||||||
|
@ -52,20 +52,24 @@ export const ContractOverview = (props: {
|
||||||
large
|
large
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPseudoNumeric && (
|
{isPseudoNumeric && (
|
||||||
<PseudoNumericResolutionOrExpectation
|
<PseudoNumericResolutionOrExpectation
|
||||||
contract={contract}
|
contract={contract}
|
||||||
className="hidden items-end xl:flex"
|
className="hidden items-end xl:flex"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{outcomeType === 'NUMERIC' && (
|
{outcomeType === 'NUMERIC' && (
|
||||||
<NumericResolutionOrExpectation
|
<NumericResolutionOrExpectation
|
||||||
contract={contract}
|
contract={contract}
|
||||||
className="hidden items-end xl:flex"
|
className="hidden items-end xl:flex"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{outcomeType === 'BOUNTY' && (
|
||||||
|
<BountyValue
|
||||||
|
contract={contract}
|
||||||
|
className="hidden items-end xl:flex"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{isBinary ? (
|
{isBinary ? (
|
||||||
|
|
|
@ -56,7 +56,8 @@ export function ContractTabs(props: {
|
||||||
tips={tips}
|
tips={tips}
|
||||||
user={user}
|
user={user}
|
||||||
mode={
|
mode={
|
||||||
contract.outcomeType === 'FREE_RESPONSE'
|
contract.outcomeType === 'FREE_RESPONSE' ||
|
||||||
|
contract.outcomeType === 'BOUNTY'
|
||||||
? 'free-response-comment-answer-groups'
|
? 'free-response-comment-answer-groups'
|
||||||
: 'comments'
|
: 'comments'
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ import { Answer } from 'common/answer'
|
||||||
import { getProbability } from 'common/calculate'
|
import { getProbability } from 'common/calculate'
|
||||||
import { getValueFromBucket } from 'common/calculate-dpm'
|
import { getValueFromBucket } from 'common/calculate-dpm'
|
||||||
import {
|
import {
|
||||||
|
AnswerContract,
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
|
BountyContract,
|
||||||
Contract,
|
Contract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
MultipleChoiceContract,
|
MultipleChoiceContract,
|
||||||
|
@ -77,7 +79,7 @@ export function BinaryContractOutcomeLabel(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FreeResponseOutcomeLabel(props: {
|
export function FreeResponseOutcomeLabel(props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: AnswerContract
|
||||||
resolution: string | 'CANCEL' | 'MKT'
|
resolution: string | 'CANCEL' | 'MKT'
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
answerClassName?: string
|
answerClassName?: string
|
||||||
|
|
|
@ -88,3 +88,7 @@ export function acceptChallenge(params: any) {
|
||||||
export function getCurrentUser(params: any) {
|
export function getCurrentUser(params: any) {
|
||||||
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
|
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addBounty(params: any) {
|
||||||
|
return call(getFunctionUrl('addbounty'), 'POST', params)
|
||||||
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ import { listUsers } from 'web/lib/firebase/users'
|
||||||
import { FeedComment } from 'web/components/feed/feed-comments'
|
import { FeedComment } from 'web/components/feed/feed-comments'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
import { FeedBet } from 'web/components/feed/feed-bets'
|
||||||
|
import { BountyBox } from 'web/components/bounty/bounty-box'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: {
|
export async function getStaticPropz(props: {
|
||||||
|
@ -116,30 +117,38 @@ export function ContractPageSidebar(props: {
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
const isNumeric = outcomeType === 'NUMERIC'
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
|
const isBounty = outcomeType === 'BOUNTY'
|
||||||
const allowTrade = tradingAllowed(contract)
|
const allowTrade = tradingAllowed(contract)
|
||||||
const allowResolve = !isResolved && isCreator && !!user
|
const allowResolve = !isResolved && isCreator && !!user
|
||||||
const hasSidePanel =
|
const hasSidePanel =
|
||||||
(isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve)
|
(isBinary || isNumeric || isPseudoNumeric || isBounty) &&
|
||||||
|
(allowTrade || allowResolve)
|
||||||
|
if (!hasSidePanel) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return hasSidePanel ? (
|
return (
|
||||||
<Col className="gap-4">
|
<Col className="gap-4">
|
||||||
{allowTrade &&
|
{allowTrade &&
|
||||||
(isNumeric ? (
|
(isNumeric ? (
|
||||||
<NumericBetPanel className="hidden xl:flex" contract={contract} />
|
<NumericBetPanel className="hidden xl:flex" contract={contract} />
|
||||||
|
) : isBounty ? (
|
||||||
|
<BountyBox contract={contract} user={user ?? null} />
|
||||||
) : (
|
) : (
|
||||||
<BetPanel
|
<BetPanel
|
||||||
className="hidden xl:flex"
|
className="hidden xl:flex"
|
||||||
contract={contract as CPMMBinaryContract}
|
contract={contract as CPMMBinaryContract}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{allowResolve &&
|
{allowResolve &&
|
||||||
(isNumeric || isPseudoNumeric ? (
|
(isNumeric || isPseudoNumeric ? (
|
||||||
<NumericResolutionPanel creator={user} contract={contract} />
|
<NumericResolutionPanel creator={user} contract={contract} />
|
||||||
) : (
|
) : isBinary ? (
|
||||||
<ResolutionPanel creator={user} contract={contract} />
|
<ResolutionPanel creator={user} contract={contract} />
|
||||||
))}
|
) : null)}
|
||||||
</Col>
|
</Col>
|
||||||
) : null
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContractPageContent(
|
export function ContractPageContent(
|
||||||
|
@ -227,7 +236,8 @@ export function ContractPageContent(
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(outcomeType === 'FREE_RESPONSE' ||
|
{(outcomeType === 'FREE_RESPONSE' ||
|
||||||
outcomeType === 'MULTIPLE_CHOICE') && (
|
outcomeType === 'MULTIPLE_CHOICE' ||
|
||||||
|
outcomeType === 'BOUNTY') && (
|
||||||
<>
|
<>
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
<AnswersPanel contract={contract} />
|
<AnswersPanel contract={contract} />
|
||||||
|
|
|
@ -56,12 +56,6 @@ export default function Create(props: { auth: { user: User } }) {
|
||||||
const { user } = props.auth
|
const { user } = props.auth
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const params = router.query as NewQuestionParams
|
const params = router.query as NewQuestionParams
|
||||||
// TODO: Not sure why Question is pulled out as its own component;
|
|
||||||
// Maybe merge into newContract and then we don't need useEffect here.
|
|
||||||
const [question, setQuestion] = useState('')
|
|
||||||
useEffect(() => {
|
|
||||||
setQuestion(params.q ?? '')
|
|
||||||
}, [params.q])
|
|
||||||
|
|
||||||
if (!router.isReady) return <div />
|
if (!router.isReady) return <div />
|
||||||
|
|
||||||
|
@ -76,26 +70,7 @@ export default function Create(props: { auth: { user: User } }) {
|
||||||
<div className="rounded-lg px-6 py-4 sm:py-0">
|
<div className="rounded-lg px-6 py-4 sm:py-0">
|
||||||
<Title className="!mt-0" text="Create a market" />
|
<Title className="!mt-0" text="Create a market" />
|
||||||
|
|
||||||
<form>
|
<NewContract params={params} creator={user} />
|
||||||
<div className="form-control w-full">
|
|
||||||
<label className="label">
|
|
||||||
<span className="mb-1">
|
|
||||||
Question<span className={'text-red-700'}>*</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Textarea
|
|
||||||
placeholder="e.g. Will the Democrats win the 2024 US presidential election?"
|
|
||||||
className="input input-bordered resize-none"
|
|
||||||
autoFocus
|
|
||||||
maxLength={MAX_QUESTION_LENGTH}
|
|
||||||
value={question}
|
|
||||||
onChange={(e) => setQuestion(e.target.value || '')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<Spacer h={6} />
|
|
||||||
<NewContract question={question} params={params} creator={user} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -103,12 +78,9 @@ export default function Create(props: { auth: { user: User } }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow user to create a new contract
|
// Allow user to create a new contract
|
||||||
export function NewContract(props: {
|
function NewContract(props: { creator: User; params?: NewQuestionParams }) {
|
||||||
creator: User
|
const { creator, params } = props
|
||||||
question: string
|
const [question, setQuestion] = useState('')
|
||||||
params?: NewQuestionParams
|
|
||||||
}) {
|
|
||||||
const { creator, question, params } = props
|
|
||||||
const { groupId, initValue } = params ?? {}
|
const { groupId, initValue } = params ?? {}
|
||||||
const [outcomeType, setOutcomeType] = useState<outcomeType>(
|
const [outcomeType, setOutcomeType] = useState<outcomeType>(
|
||||||
(params?.outcomeType as outcomeType) ?? 'BINARY'
|
(params?.outcomeType as outcomeType) ?? 'BINARY'
|
||||||
|
@ -131,6 +103,24 @@ export function NewContract(props: {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [creator.id, groupId])
|
}, [creator.id, groupId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setQuestion(params?.q ?? '')
|
||||||
|
}, [params?.q])
|
||||||
|
|
||||||
|
const questionLabel = outcomeType === 'BOUNTY' ? 'Bounty' : 'Question'
|
||||||
|
const placeholders = {
|
||||||
|
BINARY: 'e.g. Will the Democrats win the 2024 US presidential election?',
|
||||||
|
MULTIPLE_CHOICE:
|
||||||
|
'e.g. Which basketball team will be the March Madness champion?',
|
||||||
|
FREE_RESPONSE: 'e.g. What programming language should I learn next?',
|
||||||
|
NUMERIC: '', // Numeric type is deprecated
|
||||||
|
PSEUDO_NUMERIC:
|
||||||
|
'e.g. How many people will show up to Taco Tuesday this week?',
|
||||||
|
BOUNTY: 'e.g. Add Dark Mode to Manifold Markets',
|
||||||
|
}
|
||||||
|
const placeholder = placeholders[outcomeType] ?? placeholders['BINARY']
|
||||||
|
|
||||||
const [ante, _setAnte] = useState(FIXED_ANTE)
|
const [ante, _setAnte] = useState(FIXED_ANTE)
|
||||||
|
|
||||||
// If params.closeTime is set, extract out the specified date and time
|
// If params.closeTime is set, extract out the specified date and time
|
||||||
|
@ -255,7 +245,7 @@ export function NewContract(props: {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="mb-1">Answer type</span>
|
<span className="mb-1">Market type</span>
|
||||||
</label>
|
</label>
|
||||||
<ChoicesToggleGroup
|
<ChoicesToggleGroup
|
||||||
currentChoice={outcomeType}
|
currentChoice={outcomeType}
|
||||||
|
@ -272,6 +262,7 @@ export function NewContract(props: {
|
||||||
'Multiple choice': 'MULTIPLE_CHOICE',
|
'Multiple choice': 'MULTIPLE_CHOICE',
|
||||||
'Free response': 'FREE_RESPONSE',
|
'Free response': 'FREE_RESPONSE',
|
||||||
Numeric: 'PSEUDO_NUMERIC',
|
Numeric: 'PSEUDO_NUMERIC',
|
||||||
|
Bounty: 'BOUNTY',
|
||||||
}}
|
}}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
className={'col-span-4'}
|
className={'col-span-4'}
|
||||||
|
@ -284,6 +275,27 @@ export function NewContract(props: {
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">
|
||||||
|
{questionLabel}
|
||||||
|
<span className={'text-red-700'}>*</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="input input-bordered resize-none"
|
||||||
|
autoFocus
|
||||||
|
maxLength={MAX_QUESTION_LENGTH}
|
||||||
|
value={question}
|
||||||
|
onChange={(e) => setQuestion(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<Spacer h={6} />
|
||||||
|
|
||||||
{outcomeType === 'MULTIPLE_CHOICE' && (
|
{outcomeType === 'MULTIPLE_CHOICE' && (
|
||||||
<MultipleChoiceAnswers answers={answers} setAnswers={setAnswers} />
|
<MultipleChoiceAnswers answers={answers} setAnswers={setAnswers} />
|
||||||
)}
|
)}
|
||||||
|
@ -380,7 +392,7 @@ export function NewContract(props: {
|
||||||
|
|
||||||
<div className="form-control mb-1 items-start">
|
<div className="form-control mb-1 items-start">
|
||||||
<label className="label mb-1 gap-2">
|
<label className="label mb-1 gap-2">
|
||||||
<span>Question closes in</span>
|
<span>{questionLabel} closes in</span>
|
||||||
<InfoTooltip text="Betting will be halted after this time (local timezone)." />
|
<InfoTooltip text="Betting will be halted after this time (local timezone)." />
|
||||||
</label>
|
</label>
|
||||||
<Row className={'w-full items-center gap-2'}>
|
<Row className={'w-full items-center gap-2'}>
|
||||||
|
@ -436,7 +448,7 @@ export function NewContract(props: {
|
||||||
<Row className="items-end justify-between">
|
<Row className="items-end justify-between">
|
||||||
<div className="form-control mb-1 items-start">
|
<div className="form-control mb-1 items-start">
|
||||||
<label className="label mb-1 gap-2">
|
<label className="label mb-1 gap-2">
|
||||||
<span>Cost</span>
|
<span>{outcomeType === 'BOUNTY' ? 'Initial Bounty' : 'Cost'}</span>
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
text={`Cost to create your question. This amount is used to subsidize betting.`}
|
text={`Cost to create your question. This amount is used to subsidize betting.`}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user