Merge branch 'main' into austin/dc-hackathon
This commit is contained in:
commit
0d86059bc6
|
@ -1,9 +1,11 @@
|
||||||
import { last, sortBy, sum, sumBy } from 'lodash'
|
import { last, sortBy, sum, sumBy } from 'lodash'
|
||||||
import { calculatePayout } from './calculate'
|
import { calculatePayout } from './calculate'
|
||||||
import { Bet } from './bet'
|
import { Bet, LimitBet } from './bet'
|
||||||
import { Contract } from './contract'
|
import { Contract, CPMMContract, DPMContract } from './contract'
|
||||||
import { PortfolioMetrics, User } from './user'
|
import { PortfolioMetrics, User } from './user'
|
||||||
import { DAY_MS } from './util/time'
|
import { DAY_MS } from './util/time'
|
||||||
|
import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet'
|
||||||
|
import { getCpmmProbability } from './calculate-cpmm'
|
||||||
|
|
||||||
const computeInvestmentValue = (
|
const computeInvestmentValue = (
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
|
@ -40,6 +42,58 @@ export const computeInvestmentValueCustomProb = (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const computeElasticity = (
|
||||||
|
bets: Bet[],
|
||||||
|
contract: Contract,
|
||||||
|
betAmount = 50
|
||||||
|
) => {
|
||||||
|
const { mechanism, outcomeType } = contract
|
||||||
|
return mechanism === 'cpmm-1' &&
|
||||||
|
(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC')
|
||||||
|
? computeBinaryCpmmElasticity(bets, contract, betAmount)
|
||||||
|
: computeDpmElasticity(contract, betAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computeBinaryCpmmElasticity = (
|
||||||
|
bets: Bet[],
|
||||||
|
contract: CPMMContract,
|
||||||
|
betAmount: number
|
||||||
|
) => {
|
||||||
|
const limitBets = bets
|
||||||
|
.filter(
|
||||||
|
(b) =>
|
||||||
|
!b.isFilled && !b.isSold && !b.isRedemption && !b.sale && !b.isCancelled
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.createdTime - b.createdTime)
|
||||||
|
|
||||||
|
const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo(
|
||||||
|
'YES',
|
||||||
|
betAmount,
|
||||||
|
contract,
|
||||||
|
undefined,
|
||||||
|
limitBets as LimitBet[]
|
||||||
|
)
|
||||||
|
const resultYes = getCpmmProbability(poolY, pY)
|
||||||
|
|
||||||
|
const { newPool: poolN, newP: pN } = getBinaryCpmmBetInfo(
|
||||||
|
'NO',
|
||||||
|
betAmount,
|
||||||
|
contract,
|
||||||
|
undefined,
|
||||||
|
limitBets as LimitBet[]
|
||||||
|
)
|
||||||
|
const resultNo = getCpmmProbability(poolN, pN)
|
||||||
|
|
||||||
|
return resultYes - resultNo
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computeDpmElasticity = (
|
||||||
|
contract: DPMContract,
|
||||||
|
betAmount: number
|
||||||
|
) => {
|
||||||
|
return getNewMultiBetInfo('', 2 * betAmount, contract).newBet.probAfter
|
||||||
|
}
|
||||||
|
|
||||||
const computeTotalPool = (userContracts: Contract[], startTime = 0) => {
|
const computeTotalPool = (userContracts: Contract[], startTime = 0) => {
|
||||||
const periodFilteredContracts = userContracts.filter(
|
const periodFilteredContracts = userContracts.filter(
|
||||||
(contract) => contract.createdTime >= startTime
|
(contract) => contract.createdTime >= startTime
|
||||||
|
|
|
@ -49,6 +49,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
volume: number
|
volume: number
|
||||||
volume24Hours: number
|
volume24Hours: number
|
||||||
volume7Days: number
|
volume7Days: number
|
||||||
|
elasticity: number
|
||||||
|
|
||||||
collectedFees: Fees
|
collectedFees: Fees
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,7 @@ import {
|
||||||
import {
|
import {
|
||||||
CPMMBinaryContract,
|
CPMMBinaryContract,
|
||||||
DPMBinaryContract,
|
DPMBinaryContract,
|
||||||
FreeResponseContract,
|
DPMContract,
|
||||||
MultipleChoiceContract,
|
|
||||||
NumericContract,
|
NumericContract,
|
||||||
PseudoNumericContract,
|
PseudoNumericContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
|
@ -325,7 +324,7 @@ export const getNewBinaryDpmBetInfo = (
|
||||||
export const getNewMultiBetInfo = (
|
export const getNewMultiBetInfo = (
|
||||||
outcome: string,
|
outcome: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: DPMContract
|
||||||
) => {
|
) => {
|
||||||
const { pool, totalShares, totalBets } = contract
|
const { pool, totalShares, totalBets } = contract
|
||||||
|
|
||||||
|
|
|
@ -70,6 +70,7 @@ export function getNewContract(
|
||||||
volume: 0,
|
volume: 0,
|
||||||
volume24Hours: 0,
|
volume24Hours: 0,
|
||||||
volume7Days: 0,
|
volume7Days: 0,
|
||||||
|
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
|
||||||
|
|
||||||
collectedFees: {
|
collectedFees: {
|
||||||
creatorFee: 0,
|
creatorFee: 0,
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import { slugify } from '../../common/util/slugify'
|
import { slugify } from '../../common/util/slugify'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
|
|
||||||
import { isProd } from './utils'
|
import { chargeUser, getContract, isProd } from './utils'
|
||||||
import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api'
|
import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api'
|
||||||
|
|
||||||
import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy'
|
import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy'
|
||||||
|
@ -36,7 +36,7 @@ import { getPseudoProbability } from '../../common/pseudo-numeric'
|
||||||
import { JSONContent } from '@tiptap/core'
|
import { JSONContent } from '@tiptap/core'
|
||||||
import { uniq, zip } from 'lodash'
|
import { uniq, zip } from 'lodash'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { FieldValue, Transaction } from 'firebase-admin/firestore'
|
import { FieldValue } from 'firebase-admin/firestore'
|
||||||
|
|
||||||
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
|
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
|
||||||
z.intersection(
|
z.intersection(
|
||||||
|
@ -107,242 +107,229 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
|
||||||
visibility = 'public',
|
visibility = 'public',
|
||||||
} = validate(bodySchema, body)
|
} = validate(bodySchema, body)
|
||||||
|
|
||||||
return await firestore.runTransaction(async (trans) => {
|
let min, max, initialProb, isLogScale, answers
|
||||||
let min, max, initialProb, isLogScale, answers
|
|
||||||
|
|
||||||
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
||||||
let initialValue
|
let initialValue
|
||||||
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
|
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
|
||||||
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
|
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
|
||||||
throw new APIError(400, 'Invalid range.')
|
throw new APIError(400, 'Invalid range.')
|
||||||
|
|
||||||
initialProb =
|
initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100
|
||||||
getPseudoProbability(initialValue, min, max, isLogScale) * 100
|
|
||||||
|
|
||||||
if (initialProb < 1 || initialProb > 99)
|
if (initialProb < 1 || initialProb > 99)
|
||||||
if (outcomeType === 'PSEUDO_NUMERIC')
|
if (outcomeType === 'PSEUDO_NUMERIC')
|
||||||
throw new APIError(
|
|
||||||
400,
|
|
||||||
`Initial value is too ${initialProb < 1 ? 'low' : 'high'}`
|
|
||||||
)
|
|
||||||
else throw new APIError(400, 'Invalid initial probability.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outcomeType === 'BINARY') {
|
|
||||||
;({ initialProb } = validate(binarySchema, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outcomeType === 'MULTIPLE_CHOICE') {
|
|
||||||
;({ answers } = validate(multipleChoiceSchema, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
const userDoc = await trans.get(firestore.collection('users').doc(auth.uid))
|
|
||||||
if (!userDoc.exists) {
|
|
||||||
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
|
||||||
}
|
|
||||||
const user = userDoc.data() as User
|
|
||||||
|
|
||||||
const ante = FIXED_ANTE
|
|
||||||
const deservesFreeMarket =
|
|
||||||
(user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX
|
|
||||||
// TODO: this is broken because it's not in a transaction
|
|
||||||
if (ante > user.balance && !deservesFreeMarket)
|
|
||||||
throw new APIError(400, `Balance must be at least ${ante}.`)
|
|
||||||
|
|
||||||
let group: Group | null = null
|
|
||||||
if (groupId) {
|
|
||||||
const groupDocRef = firestore.collection('groups').doc(groupId)
|
|
||||||
const groupDoc = await trans.get(groupDocRef)
|
|
||||||
if (!groupDoc.exists) {
|
|
||||||
throw new APIError(400, 'No group exists with the given group ID.')
|
|
||||||
}
|
|
||||||
|
|
||||||
group = groupDoc.data() as Group
|
|
||||||
const groupMembersSnap = await trans.get(
|
|
||||||
firestore.collection(`groups/${groupId}/groupMembers`)
|
|
||||||
)
|
|
||||||
const groupMemberDocs = groupMembersSnap.docs.map(
|
|
||||||
(doc) => doc.data() as { userId: string; createdTime: number }
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
!groupMemberDocs.map((m) => m.userId).includes(user.id) &&
|
|
||||||
!group.anyoneCanJoin &&
|
|
||||||
group.creatorId !== user.id
|
|
||||||
) {
|
|
||||||
throw new APIError(
|
throw new APIError(
|
||||||
400,
|
400,
|
||||||
'User must be a member/creator of the group or group must be open to add markets to it.'
|
`Initial value is too ${initialProb < 1 ? 'low' : 'high'}`
|
||||||
)
|
)
|
||||||
}
|
else throw new APIError(400, 'Invalid initial probability.')
|
||||||
}
|
}
|
||||||
const slug = await getSlug(trans, question)
|
|
||||||
const contractRef = firestore.collection('contracts').doc()
|
|
||||||
|
|
||||||
console.log(
|
if (outcomeType === 'BINARY') {
|
||||||
'creating contract for',
|
;({ initialProb } = validate(binarySchema, body))
|
||||||
user.username,
|
}
|
||||||
'on',
|
|
||||||
question,
|
|
||||||
'ante:',
|
|
||||||
ante || 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// convert string descriptions into JSONContent
|
if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||||
const newDescription =
|
;({ answers } = validate(multipleChoiceSchema, body))
|
||||||
!description || typeof description === 'string'
|
}
|
||||||
? {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'paragraph',
|
|
||||||
content: [{ type: 'text', text: description || ' ' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: description
|
|
||||||
|
|
||||||
const contract = getNewContract(
|
const userDoc = await firestore.collection('users').doc(auth.uid).get()
|
||||||
contractRef.id,
|
if (!userDoc.exists) {
|
||||||
slug,
|
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
||||||
user,
|
}
|
||||||
question,
|
const user = userDoc.data() as User
|
||||||
outcomeType,
|
|
||||||
newDescription,
|
|
||||||
initialProb ?? 0,
|
|
||||||
ante,
|
|
||||||
closeTime.getTime(),
|
|
||||||
tags ?? [],
|
|
||||||
NUMERIC_BUCKET_COUNT,
|
|
||||||
min ?? 0,
|
|
||||||
max ?? 0,
|
|
||||||
isLogScale ?? false,
|
|
||||||
answers ?? [],
|
|
||||||
visibility
|
|
||||||
)
|
|
||||||
|
|
||||||
const providerId = deservesFreeMarket
|
const ante = FIXED_ANTE
|
||||||
? isProd()
|
const deservesFreeMarket =
|
||||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
(user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX
|
||||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
// TODO: this is broken because it's not in a transaction
|
||||||
: user.id
|
if (ante > user.balance && !deservesFreeMarket)
|
||||||
|
throw new APIError(400, `Balance must be at least ${ante}.`)
|
||||||
|
|
||||||
if (ante) {
|
let group: Group | null = null
|
||||||
const delta = FieldValue.increment(-ante)
|
if (groupId) {
|
||||||
const providerDoc = firestore.collection('users').doc(providerId)
|
const groupDocRef = firestore.collection('groups').doc(groupId)
|
||||||
await trans.update(providerDoc, { balance: delta, totalDeposits: delta })
|
const groupDoc = await groupDocRef.get()
|
||||||
|
if (!groupDoc.exists) {
|
||||||
|
throw new APIError(400, 'No group exists with the given group ID.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deservesFreeMarket) {
|
group = groupDoc.data() as Group
|
||||||
await trans.update(firestore.collection('users').doc(user.id), {
|
const groupMembersSnap = await firestore
|
||||||
freeMarketsCreated: FieldValue.increment(1),
|
.collection(`groups/${groupId}/groupMembers`)
|
||||||
|
.get()
|
||||||
|
const groupMemberDocs = groupMembersSnap.docs.map(
|
||||||
|
(doc) => doc.data() as { userId: string; createdTime: number }
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
!groupMemberDocs.map((m) => m.userId).includes(user.id) &&
|
||||||
|
!group.anyoneCanJoin &&
|
||||||
|
group.creatorId !== user.id
|
||||||
|
) {
|
||||||
|
throw new APIError(
|
||||||
|
400,
|
||||||
|
'User must be a member/creator of the group or group must be open to add markets to it.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const slug = await getSlug(question)
|
||||||
|
const contractRef = firestore.collection('contracts').doc()
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'creating contract for',
|
||||||
|
user.username,
|
||||||
|
'on',
|
||||||
|
question,
|
||||||
|
'ante:',
|
||||||
|
ante || 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// convert string descriptions into JSONContent
|
||||||
|
const newDescription =
|
||||||
|
!description || typeof description === 'string'
|
||||||
|
? {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: description || ' ' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: description
|
||||||
|
|
||||||
|
const contract = getNewContract(
|
||||||
|
contractRef.id,
|
||||||
|
slug,
|
||||||
|
user,
|
||||||
|
question,
|
||||||
|
outcomeType,
|
||||||
|
newDescription,
|
||||||
|
initialProb ?? 0,
|
||||||
|
ante,
|
||||||
|
closeTime.getTime(),
|
||||||
|
tags ?? [],
|
||||||
|
NUMERIC_BUCKET_COUNT,
|
||||||
|
min ?? 0,
|
||||||
|
max ?? 0,
|
||||||
|
isLogScale ?? false,
|
||||||
|
answers ?? [],
|
||||||
|
visibility
|
||||||
|
)
|
||||||
|
|
||||||
|
const providerId = deservesFreeMarket
|
||||||
|
? isProd()
|
||||||
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
: user.id
|
||||||
|
|
||||||
|
if (ante) await chargeUser(providerId, ante, true)
|
||||||
|
if (deservesFreeMarket)
|
||||||
|
await firestore
|
||||||
|
.collection('users')
|
||||||
|
.doc(user.id)
|
||||||
|
.update({ freeMarketsCreated: FieldValue.increment(1) })
|
||||||
|
|
||||||
|
await contractRef.create(contract)
|
||||||
|
|
||||||
|
if (group != null) {
|
||||||
|
const groupContractsSnap = await firestore
|
||||||
|
.collection(`groups/${groupId}/groupContracts`)
|
||||||
|
.get()
|
||||||
|
const groupContracts = groupContractsSnap.docs.map(
|
||||||
|
(doc) => doc.data() as { contractId: string; createdTime: number }
|
||||||
|
)
|
||||||
|
if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) {
|
||||||
|
await createGroupLinks(group, [contractRef.id], auth.uid)
|
||||||
|
const groupContractRef = firestore
|
||||||
|
.collection(`groups/${groupId}/groupContracts`)
|
||||||
|
.doc(contract.id)
|
||||||
|
await groupContractRef.set({
|
||||||
|
contractId: contract.id,
|
||||||
|
createdTime: Date.now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await contractRef.create(contract)
|
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
||||||
|
const liquidityDoc = firestore
|
||||||
|
.collection(`contracts/${contract.id}/liquidity`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
if (group != null) {
|
const lp = getCpmmInitialLiquidity(
|
||||||
const groupContractsSnap = await trans.get(
|
providerId,
|
||||||
firestore.collection(`groups/${groupId}/groupContracts`)
|
contract as CPMMBinaryContract,
|
||||||
)
|
liquidityDoc.id,
|
||||||
const groupContracts = groupContractsSnap.docs.map(
|
ante
|
||||||
(doc) => doc.data() as { contractId: string; createdTime: number }
|
)
|
||||||
|
|
||||||
|
await liquidityDoc.set(lp)
|
||||||
|
} else if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||||
|
const betCol = firestore.collection(`contracts/${contract.id}/bets`)
|
||||||
|
const betDocs = (answers ?? []).map(() => betCol.doc())
|
||||||
|
|
||||||
|
const answerCol = firestore.collection(`contracts/${contract.id}/answers`)
|
||||||
|
const answerDocs = (answers ?? []).map((_, i) =>
|
||||||
|
answerCol.doc(i.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
const { bets, answerObjects } = getMultipleChoiceAntes(
|
||||||
|
user,
|
||||||
|
contract as MultipleChoiceContract,
|
||||||
|
answers ?? [],
|
||||||
|
betDocs.map((bd) => bd.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet))
|
||||||
|
)
|
||||||
|
await Promise.all(
|
||||||
|
zip(answerObjects, answerDocs).map(([answer, doc]) =>
|
||||||
|
doc?.create(answer as Answer)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
await contractRef.update({ answers: answerObjects })
|
||||||
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||||
|
const noneAnswerDoc = firestore
|
||||||
|
.collection(`contracts/${contract.id}/answers`)
|
||||||
|
.doc('0')
|
||||||
|
|
||||||
if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) {
|
const noneAnswer = getNoneAnswer(contract.id, user)
|
||||||
await createGroupLinks(trans, group, [contractRef.id], auth.uid)
|
await noneAnswerDoc.set(noneAnswer)
|
||||||
|
|
||||||
const groupContractRef = firestore
|
const anteBetDoc = firestore
|
||||||
.collection(`groups/${groupId}/groupContracts`)
|
.collection(`contracts/${contract.id}/bets`)
|
||||||
.doc(contract.id)
|
.doc()
|
||||||
|
|
||||||
await trans.set(groupContractRef, {
|
const anteBet = getFreeAnswerAnte(
|
||||||
contractId: contract.id,
|
providerId,
|
||||||
createdTime: Date.now(),
|
contract as FreeResponseContract,
|
||||||
})
|
anteBetDoc.id
|
||||||
}
|
)
|
||||||
}
|
await anteBetDoc.set(anteBet)
|
||||||
|
} else if (outcomeType === 'NUMERIC') {
|
||||||
|
const anteBetDoc = firestore
|
||||||
|
.collection(`contracts/${contract.id}/bets`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
const anteBet = getNumericAnte(
|
||||||
const liquidityDoc = firestore
|
providerId,
|
||||||
.collection(`contracts/${contract.id}/liquidity`)
|
contract as NumericContract,
|
||||||
.doc()
|
ante,
|
||||||
|
anteBetDoc.id
|
||||||
|
)
|
||||||
|
|
||||||
const lp = getCpmmInitialLiquidity(
|
await anteBetDoc.set(anteBet)
|
||||||
providerId,
|
}
|
||||||
contract as CPMMBinaryContract,
|
|
||||||
liquidityDoc.id,
|
|
||||||
ante
|
|
||||||
)
|
|
||||||
|
|
||||||
await trans.set(liquidityDoc, lp)
|
return contract
|
||||||
} else if (outcomeType === 'MULTIPLE_CHOICE') {
|
|
||||||
const betCol = firestore.collection(`contracts/${contract.id}/bets`)
|
|
||||||
const betDocs = (answers ?? []).map(() => betCol.doc())
|
|
||||||
|
|
||||||
const answerCol = firestore.collection(`contracts/${contract.id}/answers`)
|
|
||||||
const answerDocs = (answers ?? []).map((_, i) =>
|
|
||||||
answerCol.doc(i.toString())
|
|
||||||
)
|
|
||||||
|
|
||||||
const { bets, answerObjects } = getMultipleChoiceAntes(
|
|
||||||
user,
|
|
||||||
contract as MultipleChoiceContract,
|
|
||||||
answers ?? [],
|
|
||||||
betDocs.map((bd) => bd.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
zip(bets, betDocs).map(([bet, doc]) =>
|
|
||||||
doc ? trans.create(doc, bet as Bet) : undefined
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await Promise.all(
|
|
||||||
zip(answerObjects, answerDocs).map(([answer, doc]) =>
|
|
||||||
doc ? trans.create(doc, answer as Answer) : undefined
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await trans.update(contractRef, { answers: answerObjects })
|
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
|
||||||
const noneAnswerDoc = firestore
|
|
||||||
.collection(`contracts/${contract.id}/answers`)
|
|
||||||
.doc('0')
|
|
||||||
|
|
||||||
const noneAnswer = getNoneAnswer(contract.id, user)
|
|
||||||
await trans.set(noneAnswerDoc, noneAnswer)
|
|
||||||
|
|
||||||
const anteBetDoc = firestore
|
|
||||||
.collection(`contracts/${contract.id}/bets`)
|
|
||||||
.doc()
|
|
||||||
|
|
||||||
const anteBet = getFreeAnswerAnte(
|
|
||||||
providerId,
|
|
||||||
contract as FreeResponseContract,
|
|
||||||
anteBetDoc.id
|
|
||||||
)
|
|
||||||
await trans.set(anteBetDoc, anteBet)
|
|
||||||
} else if (outcomeType === 'NUMERIC') {
|
|
||||||
const anteBetDoc = firestore
|
|
||||||
.collection(`contracts/${contract.id}/bets`)
|
|
||||||
.doc()
|
|
||||||
|
|
||||||
const anteBet = getNumericAnte(
|
|
||||||
providerId,
|
|
||||||
contract as NumericContract,
|
|
||||||
ante,
|
|
||||||
anteBetDoc.id
|
|
||||||
)
|
|
||||||
|
|
||||||
await trans.set(anteBetDoc, anteBet)
|
|
||||||
}
|
|
||||||
|
|
||||||
return contract
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSlug = async (trans: Transaction, question: string) => {
|
const getSlug = async (question: string) => {
|
||||||
const proposedSlug = slugify(question)
|
const proposedSlug = slugify(question)
|
||||||
|
|
||||||
const preexistingContract = await getContractFromSlug(trans, proposedSlug)
|
const preexistingContract = await getContractFromSlug(proposedSlug)
|
||||||
|
|
||||||
return preexistingContract
|
return preexistingContract
|
||||||
? proposedSlug + '-' + randomString()
|
? proposedSlug + '-' + randomString()
|
||||||
|
@ -351,42 +338,46 @@ const getSlug = async (trans: Transaction, question: string) => {
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
async function getContractFromSlug(trans: Transaction, slug: string) {
|
export async function getContractFromSlug(slug: string) {
|
||||||
const snap = await trans.get(
|
const snap = await firestore
|
||||||
firestore.collection('contracts').where('slug', '==', slug)
|
.collection('contracts')
|
||||||
)
|
.where('slug', '==', slug)
|
||||||
|
.get()
|
||||||
|
|
||||||
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
|
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createGroupLinks(
|
async function createGroupLinks(
|
||||||
trans: Transaction,
|
|
||||||
group: Group,
|
group: Group,
|
||||||
contractIds: string[],
|
contractIds: string[],
|
||||||
userId: string
|
userId: string
|
||||||
) {
|
) {
|
||||||
for (const contractId of contractIds) {
|
for (const contractId of contractIds) {
|
||||||
const contractRef = firestore.collection('contracts').doc(contractId)
|
const contract = await getContract(contractId)
|
||||||
const contract = (await trans.get(contractRef)).data() as Contract
|
|
||||||
|
|
||||||
if (!contract?.groupSlugs?.includes(group.slug)) {
|
if (!contract?.groupSlugs?.includes(group.slug)) {
|
||||||
await trans.update(contractRef, {
|
await firestore
|
||||||
groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]),
|
.collection('contracts')
|
||||||
})
|
.doc(contractId)
|
||||||
|
.update({
|
||||||
|
groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) {
|
if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) {
|
||||||
await trans.update(contractRef, {
|
await firestore
|
||||||
groupLinks: [
|
.collection('contracts')
|
||||||
{
|
.doc(contractId)
|
||||||
groupId: group.id,
|
.update({
|
||||||
name: group.name,
|
groupLinks: [
|
||||||
slug: group.slug,
|
{
|
||||||
userId,
|
groupId: group.id,
|
||||||
createdTime: Date.now(),
|
name: group.name,
|
||||||
} as GroupLink,
|
slug: group.slug,
|
||||||
...(contract?.groupLinks ?? []),
|
userId,
|
||||||
],
|
createdTime: Date.now(),
|
||||||
})
|
} as GroupLink,
|
||||||
|
...(contract?.groupLinks ?? []),
|
||||||
|
],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,19 +71,23 @@ export const createpost = newEndpoint({}, async (req, auth) => {
|
||||||
if (question) {
|
if (question) {
|
||||||
const closeTime = Date.now() + DAY_MS * 30 * 3
|
const closeTime = Date.now() + DAY_MS * 30 * 3
|
||||||
|
|
||||||
const result = await createMarketHelper(
|
try {
|
||||||
{
|
const result = await createMarketHelper(
|
||||||
question,
|
{
|
||||||
closeTime,
|
question,
|
||||||
outcomeType: 'BINARY',
|
closeTime,
|
||||||
visibility: 'unlisted',
|
outcomeType: 'BINARY',
|
||||||
initialProb: 50,
|
visibility: 'unlisted',
|
||||||
// Dating group!
|
initialProb: 50,
|
||||||
groupId: 'j3ZE8fkeqiKmRGumy3O1',
|
// Dating group!
|
||||||
},
|
groupId: 'j3ZE8fkeqiKmRGumy3O1',
|
||||||
auth
|
},
|
||||||
)
|
auth
|
||||||
contractSlug = result.slug
|
)
|
||||||
|
contractSlug = result.slug
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const post: Post = removeUndefinedProps({
|
const post: Post = removeUndefinedProps({
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore'
|
import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore'
|
||||||
import { isEqual, zip } from 'lodash'
|
import { isEqual, zip } from 'lodash'
|
||||||
import { UpdateSpec } from '../utils'
|
|
||||||
|
|
||||||
export type DocumentValue = {
|
export type DocumentValue = {
|
||||||
doc: DocumentSnapshot
|
doc: DocumentSnapshot
|
||||||
|
@ -54,7 +53,7 @@ export function getDiffUpdate(diff: DocumentDiff) {
|
||||||
return {
|
return {
|
||||||
doc: diff.dest.doc.ref,
|
doc: diff.dest.doc.ref,
|
||||||
fields: Object.fromEntries(zip(diff.dest.fields, diff.src.vals)),
|
fields: Object.fromEntries(zip(diff.dest.fields, diff.src.vals)),
|
||||||
} as UpdateSpec
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyDiff(transaction: Transaction, diff: DocumentDiff) {
|
export function applyDiff(transaction: Transaction, diff: DocumentDiff) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
calculateNewPortfolioMetrics,
|
calculateNewPortfolioMetrics,
|
||||||
calculateNewProfit,
|
calculateNewProfit,
|
||||||
calculateProbChanges,
|
calculateProbChanges,
|
||||||
|
computeElasticity,
|
||||||
computeVolume,
|
computeVolume,
|
||||||
} from '../../common/calculate-metrics'
|
} from '../../common/calculate-metrics'
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getProbability } from '../../common/calculate'
|
||||||
|
@ -103,6 +104,7 @@ export async function updateMetricsCore() {
|
||||||
fields: {
|
fields: {
|
||||||
volume24Hours: computeVolume(contractBets, now - DAY_MS),
|
volume24Hours: computeVolume(contractBets, now - DAY_MS),
|
||||||
volume7Days: computeVolume(contractBets, now - DAY_MS * 7),
|
volume7Days: computeVolume(contractBets, now - DAY_MS * 7),
|
||||||
|
elasticity: computeElasticity(contractBets, contract),
|
||||||
...cpmmFields,
|
...cpmmFields,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export const writeAsync = async (
|
||||||
const batch = db.batch()
|
const batch = db.batch()
|
||||||
for (const { doc, fields } of chunks[i]) {
|
for (const { doc, fields } of chunks[i]) {
|
||||||
if (operationType === 'update') {
|
if (operationType === 'update') {
|
||||||
batch.update(doc, fields)
|
batch.update(doc, fields as any)
|
||||||
} else {
|
} else {
|
||||||
batch.set(doc, fields)
|
batch.set(doc, fields)
|
||||||
}
|
}
|
||||||
|
|
10
web/components/NoSEO.tsx
Normal file
10
web/components/NoSEO.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import Head from 'next/head'
|
||||||
|
|
||||||
|
/** Exclude page from search results */
|
||||||
|
export function NoSEO() {
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
<meta name="robots" content="noindex,follow" />
|
||||||
|
</Head>
|
||||||
|
)
|
||||||
|
}
|
|
@ -157,11 +157,9 @@ 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' && tradingAllowed(contract) && (
|
||||||
tradingAllowed(contract) &&
|
<CreateAnswerPanel contract={contract} />
|
||||||
(!resolveOption || resolveOption === 'CANCEL') && (
|
)}
|
||||||
<CreateAnswerPanel contract={contract} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(user?.id === creatorId || (isAdmin && needsAdminToResolve(contract))) &&
|
{(user?.id === creatorId || (isAdmin && needsAdminToResolve(contract))) &&
|
||||||
!resolution && (
|
!resolution && (
|
||||||
|
|
|
@ -48,6 +48,7 @@ export const SORTS = [
|
||||||
{ label: 'Daily trending', value: 'daily-score' },
|
{ label: 'Daily trending', value: 'daily-score' },
|
||||||
{ label: '24h volume', value: '24-hour-vol' },
|
{ label: '24h volume', value: '24-hour-vol' },
|
||||||
{ label: 'Most popular', value: 'most-popular' },
|
{ label: 'Most popular', value: 'most-popular' },
|
||||||
|
{ label: 'Liquidity', value: 'liquidity' },
|
||||||
{ label: 'Last updated', value: 'last-updated' },
|
{ label: 'Last updated', value: 'last-updated' },
|
||||||
{ label: 'Closing soon', value: 'close-date' },
|
{ label: 'Closing soon', value: 'close-date' },
|
||||||
{ label: 'Resolve date', value: 'resolve-date' },
|
{ label: 'Resolve date', value: 'resolve-date' },
|
||||||
|
|
|
@ -37,12 +37,12 @@ export function ProbChangeTable(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="mb-4 w-full gap-4 rounded-lg md:flex-row">
|
<Col className="mb-4 w-full gap-4 rounded-lg md:flex-row">
|
||||||
<Col className="flex-1 gap-4">
|
<Col className="flex-1">
|
||||||
{filteredPositiveChanges.map((contract) => (
|
{filteredPositiveChanges.map((contract) => (
|
||||||
<ContractCardProbChange key={contract.id} contract={contract} />
|
<ContractCardProbChange key={contract.id} contract={contract} />
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="flex-1 gap-4">
|
<Col className="flex-1">
|
||||||
{filteredNegativeChanges.map((contract) => (
|
{filteredNegativeChanges.map((contract) => (
|
||||||
<ContractCardProbChange key={contract.id} contract={contract} />
|
<ContractCardProbChange key={contract.id} contract={contract} />
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -64,11 +64,11 @@ export function BetStatusText(props: {
|
||||||
}, [challengeSlug, contract.id])
|
}, [challengeSlug, contract.id])
|
||||||
|
|
||||||
const bought = amount >= 0 ? 'bought' : 'sold'
|
const bought = amount >= 0 ? 'bought' : 'sold'
|
||||||
|
const money = formatMoney(Math.abs(amount))
|
||||||
const outOfTotalAmount =
|
const outOfTotalAmount =
|
||||||
bet.limitProb !== undefined && bet.orderAmount !== undefined
|
bet.limitProb !== undefined && bet.orderAmount !== undefined
|
||||||
? ` / ${formatMoney(bet.orderAmount)}`
|
? ` of ${bet.isCancelled ? money : formatMoney(bet.orderAmount)}`
|
||||||
: ''
|
: ''
|
||||||
const money = formatMoney(Math.abs(amount))
|
|
||||||
|
|
||||||
const hadPoolMatch =
|
const hadPoolMatch =
|
||||||
(bet.limitProb === undefined ||
|
(bet.limitProb === undefined ||
|
||||||
|
@ -105,7 +105,6 @@ export function BetStatusText(props: {
|
||||||
{!hideOutcome && (
|
{!hideOutcome && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
of{' '}
|
|
||||||
<OutcomeLabel
|
<OutcomeLabel
|
||||||
outcome={outcome}
|
outcome={outcome}
|
||||||
value={(bet as any).value}
|
value={(bet as any).value}
|
||||||
|
|
|
@ -145,8 +145,6 @@ function GroupOverviewPinned(props: {
|
||||||
}) {
|
}) {
|
||||||
const { group, posts, isEditable } = props
|
const { group, posts, isEditable } = props
|
||||||
const [pinned, setPinned] = useState<JSX.Element[]>([])
|
const [pinned, setPinned] = useState<JSX.Element[]>([])
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [editMode, setEditMode] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getPinned() {
|
async function getPinned() {
|
||||||
|
@ -185,100 +183,127 @@ function GroupOverviewPinned(props: {
|
||||||
...(selectedItems as { itemId: string; type: 'contract' | 'post' }[]),
|
...(selectedItems as { itemId: string; type: 'contract' | 'post' }[]),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
setOpen(false)
|
}
|
||||||
|
|
||||||
|
function onDeleteClicked(index: number) {
|
||||||
|
const newPinned = group.pinnedItems.filter((item) => {
|
||||||
|
return item.itemId !== group.pinnedItems[index].itemId
|
||||||
|
})
|
||||||
|
updateGroup(group, { pinnedItems: newPinned })
|
||||||
}
|
}
|
||||||
|
|
||||||
return isEditable || (group.pinnedItems && group.pinnedItems.length > 0) ? (
|
return isEditable || (group.pinnedItems && group.pinnedItems.length > 0) ? (
|
||||||
pinned.length > 0 || isEditable ? (
|
<PinnedItems
|
||||||
<div>
|
posts={posts}
|
||||||
<Row className="mb-3 items-center justify-between">
|
group={group}
|
||||||
<SectionHeader label={'Pinned'} />
|
isEditable={isEditable}
|
||||||
{isEditable && (
|
pinned={pinned}
|
||||||
<Button
|
onDeleteClicked={onDeleteClicked}
|
||||||
color="gray"
|
onSubmit={onSubmit}
|
||||||
size="xs"
|
modalMessage={'Pin posts or markets to the overview of this group.'}
|
||||||
onClick={() => {
|
/>
|
||||||
setEditMode(!editMode)
|
) : (
|
||||||
}}
|
<LoadingIndicator />
|
||||||
>
|
)
|
||||||
{editMode ? (
|
}
|
||||||
'Done'
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PencilIcon className="inline h-4 w-4" />
|
|
||||||
Edit
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
<div>
|
|
||||||
<Masonry
|
|
||||||
breakpointCols={{ default: 2, 768: 1 }}
|
|
||||||
className="-ml-4 flex w-auto"
|
|
||||||
columnClassName="pl-4 bg-clip-padding"
|
|
||||||
>
|
|
||||||
{pinned.length == 0 && !editMode && (
|
|
||||||
<div className="flex flex-col items-center justify-center">
|
|
||||||
<p className="text-center text-gray-400">
|
|
||||||
No pinned items yet. Click the edit button to add some!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{pinned.map((element, index) => (
|
|
||||||
<div className="relative my-2">
|
|
||||||
{element}
|
|
||||||
|
|
||||||
{editMode && (
|
export function PinnedItems(props: {
|
||||||
<CrossIcon
|
posts: Post[]
|
||||||
onClick={() => {
|
isEditable: boolean
|
||||||
const newPinned = group.pinnedItems.filter((item) => {
|
pinned: JSX.Element[]
|
||||||
return item.itemId !== group.pinnedItems[index].itemId
|
onDeleteClicked: (index: number) => void
|
||||||
})
|
onSubmit: (selectedItems: { itemId: string; type: string }[]) => void
|
||||||
updateGroup(group, { pinnedItems: newPinned })
|
group?: Group
|
||||||
}}
|
modalMessage: string
|
||||||
/>
|
}) {
|
||||||
)}
|
const {
|
||||||
</div>
|
isEditable,
|
||||||
))}
|
pinned,
|
||||||
{editMode && group.pinnedItems && pinned.length < 6 && (
|
onDeleteClicked,
|
||||||
<div className=" py-2">
|
onSubmit,
|
||||||
<Row
|
posts,
|
||||||
className={
|
group,
|
||||||
'relative gap-3 rounded-lg border-4 border-dotted p-2 hover:cursor-pointer hover:bg-gray-100'
|
modalMessage,
|
||||||
}
|
} = props
|
||||||
>
|
const [editMode, setEditMode] = useState(false)
|
||||||
<button
|
const [open, setOpen] = useState(false)
|
||||||
className="flex w-full justify-center"
|
|
||||||
onClick={() => setOpen(true)}
|
return pinned.length > 0 || isEditable ? (
|
||||||
>
|
<div>
|
||||||
<PlusCircleIcon
|
<Row className="mb-3 items-center justify-between">
|
||||||
className="h-12 w-12 text-gray-600"
|
<SectionHeader label={'Pinned'} />
|
||||||
aria-hidden="true"
|
{isEditable && (
|
||||||
/>
|
<Button
|
||||||
</button>
|
color="gray"
|
||||||
</Row>
|
size="xs"
|
||||||
</div>
|
onClick={() => {
|
||||||
|
setEditMode(!editMode)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{editMode ? (
|
||||||
|
'Done'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PencilIcon className="inline h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Masonry>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
<PinnedSelectModal
|
</Row>
|
||||||
open={open}
|
<div>
|
||||||
group={group}
|
<Masonry
|
||||||
posts={posts}
|
breakpointCols={{ default: 2, 768: 1 }}
|
||||||
setOpen={setOpen}
|
className="-ml-4 flex w-auto"
|
||||||
title="Pin a post or market"
|
columnClassName="pl-4 bg-clip-padding"
|
||||||
description={
|
>
|
||||||
<div className={'text-md my-4 text-gray-600'}>
|
{pinned.length == 0 && !editMode && (
|
||||||
Pin posts or markets to the overview of this group.
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<p className="text-center text-gray-400">
|
||||||
|
No pinned items yet. Click the edit button to add some!
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
onSubmit={onSubmit}
|
{pinned.map((element, index) => (
|
||||||
/>
|
<div className="relative my-2">
|
||||||
|
{element}
|
||||||
|
|
||||||
|
{editMode && <CrossIcon onClick={() => onDeleteClicked(index)} />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{editMode && pinned.length < 6 && (
|
||||||
|
<div className=" py-2">
|
||||||
|
<Row
|
||||||
|
className={
|
||||||
|
'relative gap-3 rounded-lg border-4 border-dotted p-2 hover:cursor-pointer hover:bg-gray-100'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="flex w-full justify-center"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<PlusCircleIcon
|
||||||
|
className="h-12 w-12 text-gray-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Masonry>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<PinnedSelectModal
|
||||||
<LoadingIndicator />
|
open={open}
|
||||||
)
|
group={group}
|
||||||
|
posts={posts}
|
||||||
|
setOpen={setOpen}
|
||||||
|
title="Pin a post or market"
|
||||||
|
description={
|
||||||
|
<div className={'text-md my-4 text-gray-600'}>{modalMessage}</div>
|
||||||
|
}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,6 +22,7 @@ export function ManifoldLogo(props: {
|
||||||
src={darkBackground ? '/logo-white.svg' : '/logo.svg'}
|
src={darkBackground ? '/logo-white.svg' : '/logo.svg'}
|
||||||
width={45}
|
width={45}
|
||||||
height={45}
|
height={45}
|
||||||
|
alt=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!hideText &&
|
{!hideText &&
|
||||||
|
|
|
@ -11,13 +11,13 @@ function SidebarButton(props: {
|
||||||
}) {
|
}) {
|
||||||
const { text, children } = props
|
const { text, children } = props
|
||||||
return (
|
return (
|
||||||
<a className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100">
|
<button className="group flex w-full items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100">
|
||||||
<props.icon
|
<props.icon
|
||||||
className="-ml-1 mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
|
className="-ml-1 mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{text}</span>
|
<span className="truncate">{text}</span>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,8 @@ export function PinnedSelectModal(props: {
|
||||||
selectedItems: { itemId: string; type: string }[]
|
selectedItems: { itemId: string; type: string }[]
|
||||||
) => void | Promise<void>
|
) => void | Promise<void>
|
||||||
contractSearchOptions?: Partial<Parameters<typeof ContractSearch>[0]>
|
contractSearchOptions?: Partial<Parameters<typeof ContractSearch>[0]>
|
||||||
group: Group
|
|
||||||
posts: Post[]
|
posts: Post[]
|
||||||
|
group?: Group
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
|
@ -134,8 +134,8 @@ export function PinnedSelectModal(props: {
|
||||||
highlightClassName:
|
highlightClassName:
|
||||||
'!bg-indigo-100 outline outline-2 outline-indigo-300',
|
'!bg-indigo-100 outline outline-2 outline-indigo-300',
|
||||||
}}
|
}}
|
||||||
additionalFilter={{ groupSlug: group.slug }}
|
additionalFilter={group ? { groupSlug: group.slug } : undefined}
|
||||||
persistPrefix={`group-${group.slug}`}
|
persistPrefix={group ? `group-${group.slug}` : undefined}
|
||||||
headerClassName="bg-white sticky"
|
headerClassName="bg-white sticky"
|
||||||
{...contractSearchOptions}
|
{...contractSearchOptions}
|
||||||
/>
|
/>
|
||||||
|
@ -152,7 +152,7 @@ export function PinnedSelectModal(props: {
|
||||||
'!bg-indigo-100 outline outline-2 outline-indigo-300',
|
'!bg-indigo-100 outline outline-2 outline-indigo-300',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{posts.length === 0 && (
|
{posts.length == 0 && (
|
||||||
<div className="text-center text-gray-500">No posts yet</div>
|
<div className="text-center text-gray-500">No posts yet</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,6 +34,17 @@ export function UserLink(props: {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{shortName}
|
{shortName}
|
||||||
|
{BOT_USERNAMES.includes(username) && <BotBadge />}
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BOT_USERNAMES = ['v', 'ArbitrageBot']
|
||||||
|
|
||||||
|
function BotBadge() {
|
||||||
|
return (
|
||||||
|
<span className="ml-1.5 inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800">
|
||||||
|
Bot
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Post } from 'common/post'
|
import { DateDoc, Post } from 'common/post'
|
||||||
import { listenForPost } from 'web/lib/firebase/posts'
|
import { listenForDateDocs, listenForPost } from 'web/lib/firebase/posts'
|
||||||
|
|
||||||
export const usePost = (postId: string | undefined) => {
|
export const usePost = (postId: string | undefined) => {
|
||||||
const [post, setPost] = useState<Post | null | undefined>()
|
const [post, setPost] = useState<Post | null | undefined>()
|
||||||
|
@ -37,3 +37,13 @@ export const usePosts = (postIds: string[]) => {
|
||||||
)
|
)
|
||||||
.sort((a, b) => b.createdTime - a.createdTime)
|
.sort((a, b) => b.createdTime - a.createdTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useDateDocs = () => {
|
||||||
|
const [dateDocs, setDateDocs] = useState<DateDoc[]>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return listenForDateDocs(setDateDocs)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return dateDocs
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,13 @@ import {
|
||||||
where,
|
where,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { DateDoc, Post } from 'common/post'
|
import { DateDoc, Post } from 'common/post'
|
||||||
import { coll, getValue, getValues, listenForValue } from './utils'
|
import {
|
||||||
|
coll,
|
||||||
|
getValue,
|
||||||
|
getValues,
|
||||||
|
listenForValue,
|
||||||
|
listenForValues,
|
||||||
|
} from './utils'
|
||||||
import { getUserByUsername } from './users'
|
import { getUserByUsername } from './users'
|
||||||
|
|
||||||
export const posts = coll<Post>('posts')
|
export const posts = coll<Post>('posts')
|
||||||
|
@ -51,6 +57,11 @@ export async function getDateDocs() {
|
||||||
return getValues<DateDoc>(q)
|
return getValues<DateDoc>(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listenForDateDocs(setDateDocs: (dateDocs: DateDoc[]) => void) {
|
||||||
|
const q = query(posts, where('type', '==', 'date-doc'))
|
||||||
|
return listenForValues<DateDoc>(q, setDateDocs)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getDateDoc(username: string) {
|
export async function getDateDoc(username: string) {
|
||||||
const user = await getUserByUsername(username)
|
const user = await getUserByUsername(username)
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { PROD_CONFIG } from 'common/envs/prod'
|
import { PROD_CONFIG } from 'common/envs/prod'
|
||||||
|
|
||||||
if (ENV_CONFIG.domain === PROD_CONFIG.domain) {
|
if (ENV_CONFIG.domain === PROD_CONFIG.domain && typeof window !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
;(function (l, e, a, p) {
|
;(function (l, e, a, p) {
|
||||||
if (window.Sprig) return
|
if (window.Sprig) return
|
||||||
|
@ -20,7 +20,8 @@ if (ENV_CONFIG.domain === PROD_CONFIG.domain) {
|
||||||
a.async = 1
|
a.async = 1
|
||||||
a.src = e + '?id=' + S.appId
|
a.src = e + '?id=' + S.appId
|
||||||
p = l.getElementsByTagName('script')[0]
|
p = l.getElementsByTagName('script')[0]
|
||||||
p.parentNode.insertBefore(a, p)
|
ENV_CONFIG.domain === PROD_CONFIG.domain &&
|
||||||
|
p.parentNode.insertBefore(a, p)
|
||||||
})(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId)
|
})(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error initializing Sprig, please complain to Barak', error)
|
console.log('Error initializing Sprig, please complain to Barak', error)
|
||||||
|
|
|
@ -74,10 +74,7 @@ function MyApp({ Component, pageProps }: AppProps<ManifoldPageProps>) {
|
||||||
content="https://manifold.markets/logo-bg-white.png"
|
content="https://manifold.markets/logo-bg-white.png"
|
||||||
key="image2"
|
key="image2"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
|
||||||
/>
|
|
||||||
</Head>
|
</Head>
|
||||||
<AuthProvider serverUser={pageProps.auth}>
|
<AuthProvider serverUser={pageProps.auth}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html data-theme="mantic" className="min-h-screen">
|
<Html lang="en" data-theme="mantic" className="min-h-screen">
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="icon" href={ENV_CONFIG.faviconPath} />
|
<link rel="icon" href={ENV_CONFIG.faviconPath} />
|
||||||
<link
|
<link
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { PostCommentsActivity, RichEditPost } from '../post/[...slugs]'
|
||||||
import { usePost } from 'web/hooks/use-post'
|
import { usePost } from 'web/hooks/use-post'
|
||||||
import { useTipTxns } from 'web/hooks/use-tip-txns'
|
import { useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
import { useCommentsOnPost } from 'web/hooks/use-comments'
|
import { useCommentsOnPost } from 'web/hooks/use-comments'
|
||||||
|
import { NoSEO } from 'web/components/NoSEO'
|
||||||
|
|
||||||
export async function getStaticProps(props: { params: { username: string } }) {
|
export async function getStaticProps(props: { params: { username: string } }) {
|
||||||
const { username } = props.params
|
const { username } = props.params
|
||||||
|
@ -62,6 +63,7 @@ function DateDocPage(props: { creator: User; post: DateDoc }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
<NoSEO />
|
||||||
<Col className="mx-auto w-full max-w-xl gap-6 sm:mb-6">
|
<Col className="mx-auto w-full max-w-xl gap-6 sm:mb-6">
|
||||||
<SiteLink href="/date-docs">
|
<SiteLink href="/date-docs">
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
|
@ -140,15 +142,17 @@ export function DateDocPost(props: {
|
||||||
) : (
|
) : (
|
||||||
<Content content={content} />
|
<Content content={content} />
|
||||||
)}
|
)}
|
||||||
<div className="mt-4 w-full max-w-lg self-center rounded-xl bg-gradient-to-r from-blue-200 via-purple-200 to-indigo-300 p-3">
|
{contractSlug && (
|
||||||
<iframe
|
<div className="mt-4 w-full max-w-lg self-center rounded-xl bg-gradient-to-r from-blue-200 via-purple-200 to-indigo-300 p-3">
|
||||||
height="405"
|
<iframe
|
||||||
src={marketUrl}
|
height="405"
|
||||||
title=""
|
src={marketUrl}
|
||||||
frameBorder="0"
|
title=""
|
||||||
className="w-full rounded-xl bg-white p-10"
|
frameBorder="0"
|
||||||
></iframe>
|
className="w-full rounded-xl bg-white p-10"
|
||||||
</div>
|
></iframe>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,9 @@ import dayjs from 'dayjs'
|
||||||
import { MINUTE_MS } from 'common/util/time'
|
import { MINUTE_MS } from 'common/util/time'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { MAX_QUESTION_LENGTH } from 'common/contract'
|
import { MAX_QUESTION_LENGTH } from 'common/contract'
|
||||||
|
import { NoSEO } from 'web/components/NoSEO'
|
||||||
|
import ShortToggle from 'web/components/widgets/short-toggle'
|
||||||
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
|
|
||||||
export default function CreateDateDocPage() {
|
export default function CreateDateDocPage() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -25,6 +28,7 @@ export default function CreateDateDocPage() {
|
||||||
const title = `${user?.name}'s Date Doc`
|
const title = `${user?.name}'s Date Doc`
|
||||||
const subtitle = 'Manifold dating docs'
|
const subtitle = 'Manifold dating docs'
|
||||||
const [birthday, setBirthday] = useState<undefined | string>(undefined)
|
const [birthday, setBirthday] = useState<undefined | string>(undefined)
|
||||||
|
const [createMarket, setCreateMarket] = useState(true)
|
||||||
const [question, setQuestion] = useState(
|
const [question, setQuestion] = useState(
|
||||||
'Will I find a partner in the next 3 months?'
|
'Will I find a partner in the next 3 months?'
|
||||||
)
|
)
|
||||||
|
@ -37,7 +41,11 @@ export default function CreateDateDocPage() {
|
||||||
|
|
||||||
const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined
|
const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined
|
||||||
const isValid =
|
const isValid =
|
||||||
user && birthday && editor && editor.isEmpty === false && question
|
user &&
|
||||||
|
birthday &&
|
||||||
|
editor &&
|
||||||
|
editor.isEmpty === false &&
|
||||||
|
(question || !createMarket)
|
||||||
|
|
||||||
async function saveDateDoc() {
|
async function saveDateDoc() {
|
||||||
if (!user || !editor || !birthdayTime) return
|
if (!user || !editor || !birthdayTime) return
|
||||||
|
@ -45,15 +53,15 @@ export default function CreateDateDocPage() {
|
||||||
const newPost: Omit<
|
const newPost: Omit<
|
||||||
DateDoc,
|
DateDoc,
|
||||||
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
|
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
|
||||||
> & { question: string } = {
|
> & { question?: string } = removeUndefinedProps({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
content: editor.getJSON(),
|
content: editor.getJSON(),
|
||||||
bounty: 0,
|
bounty: 0,
|
||||||
birthday: birthdayTime,
|
birthday: birthdayTime,
|
||||||
type: 'date-doc',
|
type: 'date-doc',
|
||||||
question,
|
question: createMarket ? question : undefined,
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = await createPost(newPost)
|
const result = await createPost(newPost)
|
||||||
|
|
||||||
|
@ -64,6 +72,7 @@ export default function CreateDateDocPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
<NoSEO />
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
<div className="mx-auto w-full max-w-3xl">
|
||||||
<div className="rounded-lg px-6 py-4 pb-4 sm:py-0">
|
<div className="rounded-lg px-6 py-4 pb-4 sm:py-0">
|
||||||
<Row className="mb-8 items-center justify-between">
|
<Row className="mb-8 items-center justify-between">
|
||||||
|
@ -104,9 +113,13 @@ export default function CreateDateDocPage() {
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col className="gap-4">
|
<Col className="gap-4">
|
||||||
<div className="">
|
<Row className="items-center gap-4">
|
||||||
Finally, we'll create an (unlisted) prediction market!
|
<ShortToggle
|
||||||
</div>
|
on={createMarket}
|
||||||
|
setOn={(on) => setCreateMarket(on)}
|
||||||
|
/>
|
||||||
|
Create an (unlisted) prediction market attached to the date doc
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<Textarea
|
<Textarea
|
||||||
|
@ -114,6 +127,7 @@ export default function CreateDateDocPage() {
|
||||||
maxLength={MAX_QUESTION_LENGTH}
|
maxLength={MAX_QUESTION_LENGTH}
|
||||||
value={question}
|
value={question}
|
||||||
onChange={(e) => setQuestion(e.target.value || '')}
|
onChange={(e) => setQuestion(e.target.value || '')}
|
||||||
|
disabled={!createMarket}
|
||||||
/>
|
/>
|
||||||
<div className="ml-2 text-gray-500">Cost: M$100</div>
|
<div className="ml-2 text-gray-500">Cost: M$100</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { Button } from 'web/components/button'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { getUser, User } from 'web/lib/firebase/users'
|
import { getUser, User } from 'web/lib/firebase/users'
|
||||||
import { DateDocPost } from './[username]'
|
import { DateDocPost } from './[username]'
|
||||||
|
import { NoSEO } from 'web/components/NoSEO'
|
||||||
|
import { useDateDocs } from 'web/hooks/use-post'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const dateDocs = await getDateDocs()
|
const dateDocs = await getDateDocs()
|
||||||
|
@ -33,13 +35,15 @@ export default function DatePage(props: {
|
||||||
dateDocs: DateDoc[]
|
dateDocs: DateDoc[]
|
||||||
docCreators: User[]
|
docCreators: User[]
|
||||||
}) {
|
}) {
|
||||||
const { dateDocs, docCreators } = props
|
const { docCreators } = props
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
|
const dateDocs = useDateDocs() ?? props.dateDocs
|
||||||
const hasDoc = dateDocs.some((d) => d.creatorId === user?.id)
|
const hasDoc = dateDocs.some((d) => d.creatorId === user?.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
<NoSEO />
|
||||||
<div className="mx-auto w-full max-w-xl">
|
<div className="mx-auto w-full max-w-xl">
|
||||||
<Row className="items-center justify-between p-4 sm:p-0">
|
<Row className="items-center justify-between p-4 sm:p-0">
|
||||||
<Title className="!my-0 px-2 text-blue-500" text="Date docs" />
|
<Title className="!my-0 px-2 text-blue-500" text="Date docs" />
|
||||||
|
|
Loading…
Reference in New Issue
Block a user