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 { calculatePayout } from './calculate'
|
||||
import { Bet } from './bet'
|
||||
import { Contract } from './contract'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import { Contract, CPMMContract, DPMContract } from './contract'
|
||||
import { PortfolioMetrics, User } from './user'
|
||||
import { DAY_MS } from './util/time'
|
||||
import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet'
|
||||
import { getCpmmProbability } from './calculate-cpmm'
|
||||
|
||||
const computeInvestmentValue = (
|
||||
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 periodFilteredContracts = userContracts.filter(
|
||||
(contract) => contract.createdTime >= startTime
|
||||
|
|
|
@ -49,6 +49,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
volume: number
|
||||
volume24Hours: number
|
||||
volume7Days: number
|
||||
elasticity: number
|
||||
|
||||
collectedFees: Fees
|
||||
|
||||
|
|
|
@ -17,8 +17,7 @@ import {
|
|||
import {
|
||||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
DPMContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
|
@ -325,7 +324,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
export const getNewMultiBetInfo = (
|
||||
outcome: string,
|
||||
amount: number,
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
contract: DPMContract
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@ export function getNewContract(
|
|||
volume: 0,
|
||||
volume24Hours: 0,
|
||||
volume7Days: 0,
|
||||
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
|
||||
|
||||
collectedFees: {
|
||||
creatorFee: 0,
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { slugify } from '../../common/util/slugify'
|
||||
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 { 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 { uniq, zip } from 'lodash'
|
||||
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(() =>
|
||||
z.intersection(
|
||||
|
@ -107,242 +107,229 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
|
|||
visibility = 'public',
|
||||
} = 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') {
|
||||
let initialValue
|
||||
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
|
||||
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
|
||||
throw new APIError(400, 'Invalid range.')
|
||||
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
||||
let initialValue
|
||||
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
|
||||
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
|
||||
throw new APIError(400, 'Invalid range.')
|
||||
|
||||
initialProb =
|
||||
getPseudoProbability(initialValue, min, max, isLogScale) * 100
|
||||
initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100
|
||||
|
||||
if (initialProb < 1 || initialProb > 99)
|
||||
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
|
||||
) {
|
||||
if (initialProb < 1 || initialProb > 99)
|
||||
if (outcomeType === 'PSEUDO_NUMERIC')
|
||||
throw new APIError(
|
||||
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'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
const slug = await getSlug(trans, question)
|
||||
const contractRef = firestore.collection('contracts').doc()
|
||||
else throw new APIError(400, 'Invalid initial probability.')
|
||||
}
|
||||
|
||||
console.log(
|
||||
'creating contract for',
|
||||
user.username,
|
||||
'on',
|
||||
question,
|
||||
'ante:',
|
||||
ante || 0
|
||||
)
|
||||
if (outcomeType === 'BINARY') {
|
||||
;({ initialProb } = validate(binarySchema, body))
|
||||
}
|
||||
|
||||
// convert string descriptions into JSONContent
|
||||
const newDescription =
|
||||
!description || typeof description === 'string'
|
||||
? {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: description || ' ' }],
|
||||
},
|
||||
],
|
||||
}
|
||||
: description
|
||||
if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||
;({ answers } = validate(multipleChoiceSchema, body))
|
||||
}
|
||||
|
||||
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 userDoc = await firestore.collection('users').doc(auth.uid).get()
|
||||
if (!userDoc.exists) {
|
||||
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
||||
}
|
||||
const user = userDoc.data() as User
|
||||
|
||||
const providerId = deservesFreeMarket
|
||||
? isProd()
|
||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: user.id
|
||||
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}.`)
|
||||
|
||||
if (ante) {
|
||||
const delta = FieldValue.increment(-ante)
|
||||
const providerDoc = firestore.collection('users').doc(providerId)
|
||||
await trans.update(providerDoc, { balance: delta, totalDeposits: delta })
|
||||
let group: Group | null = null
|
||||
if (groupId) {
|
||||
const groupDocRef = firestore.collection('groups').doc(groupId)
|
||||
const groupDoc = await groupDocRef.get()
|
||||
if (!groupDoc.exists) {
|
||||
throw new APIError(400, 'No group exists with the given group ID.')
|
||||
}
|
||||
|
||||
if (deservesFreeMarket) {
|
||||
await trans.update(firestore.collection('users').doc(user.id), {
|
||||
freeMarketsCreated: FieldValue.increment(1),
|
||||
group = groupDoc.data() as Group
|
||||
const groupMembersSnap = await firestore
|
||||
.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 groupContractsSnap = await trans.get(
|
||||
firestore.collection(`groups/${groupId}/groupContracts`)
|
||||
)
|
||||
const groupContracts = groupContractsSnap.docs.map(
|
||||
(doc) => doc.data() as { contractId: string; createdTime: number }
|
||||
const lp = getCpmmInitialLiquidity(
|
||||
providerId,
|
||||
contract as CPMMBinaryContract,
|
||||
liquidityDoc.id,
|
||||
ante
|
||||
)
|
||||
|
||||
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)) {
|
||||
await createGroupLinks(trans, group, [contractRef.id], auth.uid)
|
||||
const noneAnswer = getNoneAnswer(contract.id, user)
|
||||
await noneAnswerDoc.set(noneAnswer)
|
||||
|
||||
const groupContractRef = firestore
|
||||
.collection(`groups/${groupId}/groupContracts`)
|
||||
.doc(contract.id)
|
||||
const anteBetDoc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
|
||||
await trans.set(groupContractRef, {
|
||||
contractId: contract.id,
|
||||
createdTime: Date.now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
const anteBet = getFreeAnswerAnte(
|
||||
providerId,
|
||||
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 liquidityDoc = firestore
|
||||
.collection(`contracts/${contract.id}/liquidity`)
|
||||
.doc()
|
||||
const anteBet = getNumericAnte(
|
||||
providerId,
|
||||
contract as NumericContract,
|
||||
ante,
|
||||
anteBetDoc.id
|
||||
)
|
||||
|
||||
const lp = getCpmmInitialLiquidity(
|
||||
providerId,
|
||||
contract as CPMMBinaryContract,
|
||||
liquidityDoc.id,
|
||||
ante
|
||||
)
|
||||
await anteBetDoc.set(anteBet)
|
||||
}
|
||||
|
||||
await trans.set(liquidityDoc, 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 ? 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
|
||||
})
|
||||
return contract
|
||||
}
|
||||
|
||||
const getSlug = async (trans: Transaction, question: string) => {
|
||||
const getSlug = async (question: string) => {
|
||||
const proposedSlug = slugify(question)
|
||||
|
||||
const preexistingContract = await getContractFromSlug(trans, proposedSlug)
|
||||
const preexistingContract = await getContractFromSlug(proposedSlug)
|
||||
|
||||
return preexistingContract
|
||||
? proposedSlug + '-' + randomString()
|
||||
|
@ -351,42 +338,46 @@ const getSlug = async (trans: Transaction, question: string) => {
|
|||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function getContractFromSlug(trans: Transaction, slug: string) {
|
||||
const snap = await trans.get(
|
||||
firestore.collection('contracts').where('slug', '==', slug)
|
||||
)
|
||||
export async function getContractFromSlug(slug: string) {
|
||||
const snap = await firestore
|
||||
.collection('contracts')
|
||||
.where('slug', '==', slug)
|
||||
.get()
|
||||
|
||||
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
|
||||
}
|
||||
|
||||
async function createGroupLinks(
|
||||
trans: Transaction,
|
||||
group: Group,
|
||||
contractIds: string[],
|
||||
userId: string
|
||||
) {
|
||||
for (const contractId of contractIds) {
|
||||
const contractRef = firestore.collection('contracts').doc(contractId)
|
||||
const contract = (await trans.get(contractRef)).data() as Contract
|
||||
|
||||
const contract = await getContract(contractId)
|
||||
if (!contract?.groupSlugs?.includes(group.slug)) {
|
||||
await trans.update(contractRef, {
|
||||
groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]),
|
||||
})
|
||||
await firestore
|
||||
.collection('contracts')
|
||||
.doc(contractId)
|
||||
.update({
|
||||
groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]),
|
||||
})
|
||||
}
|
||||
if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) {
|
||||
await trans.update(contractRef, {
|
||||
groupLinks: [
|
||||
{
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
slug: group.slug,
|
||||
userId,
|
||||
createdTime: Date.now(),
|
||||
} as GroupLink,
|
||||
...(contract?.groupLinks ?? []),
|
||||
],
|
||||
})
|
||||
await firestore
|
||||
.collection('contracts')
|
||||
.doc(contractId)
|
||||
.update({
|
||||
groupLinks: [
|
||||
{
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
slug: group.slug,
|
||||
userId,
|
||||
createdTime: Date.now(),
|
||||
} as GroupLink,
|
||||
...(contract?.groupLinks ?? []),
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,19 +71,23 @@ export const createpost = newEndpoint({}, async (req, auth) => {
|
|||
if (question) {
|
||||
const closeTime = Date.now() + DAY_MS * 30 * 3
|
||||
|
||||
const result = await createMarketHelper(
|
||||
{
|
||||
question,
|
||||
closeTime,
|
||||
outcomeType: 'BINARY',
|
||||
visibility: 'unlisted',
|
||||
initialProb: 50,
|
||||
// Dating group!
|
||||
groupId: 'j3ZE8fkeqiKmRGumy3O1',
|
||||
},
|
||||
auth
|
||||
)
|
||||
contractSlug = result.slug
|
||||
try {
|
||||
const result = await createMarketHelper(
|
||||
{
|
||||
question,
|
||||
closeTime,
|
||||
outcomeType: 'BINARY',
|
||||
visibility: 'unlisted',
|
||||
initialProb: 50,
|
||||
// Dating group!
|
||||
groupId: 'j3ZE8fkeqiKmRGumy3O1',
|
||||
},
|
||||
auth
|
||||
)
|
||||
contractSlug = result.slug
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const post: Post = removeUndefinedProps({
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore'
|
||||
import { isEqual, zip } from 'lodash'
|
||||
import { UpdateSpec } from '../utils'
|
||||
|
||||
export type DocumentValue = {
|
||||
doc: DocumentSnapshot
|
||||
|
@ -54,7 +53,7 @@ export function getDiffUpdate(diff: DocumentDiff) {
|
|||
return {
|
||||
doc: diff.dest.doc.ref,
|
||||
fields: Object.fromEntries(zip(diff.dest.fields, diff.src.vals)),
|
||||
} as UpdateSpec
|
||||
}
|
||||
}
|
||||
|
||||
export function applyDiff(transaction: Transaction, diff: DocumentDiff) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
calculateNewPortfolioMetrics,
|
||||
calculateNewProfit,
|
||||
calculateProbChanges,
|
||||
computeElasticity,
|
||||
computeVolume,
|
||||
} from '../../common/calculate-metrics'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
|
@ -103,6 +104,7 @@ export async function updateMetricsCore() {
|
|||
fields: {
|
||||
volume24Hours: computeVolume(contractBets, now - DAY_MS),
|
||||
volume7Days: computeVolume(contractBets, now - DAY_MS * 7),
|
||||
elasticity: computeElasticity(contractBets, contract),
|
||||
...cpmmFields,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export const writeAsync = async (
|
|||
const batch = db.batch()
|
||||
for (const { doc, fields } of chunks[i]) {
|
||||
if (operationType === 'update') {
|
||||
batch.update(doc, fields)
|
||||
batch.update(doc, fields as any)
|
||||
} else {
|
||||
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>
|
||||
)}
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' &&
|
||||
tradingAllowed(contract) &&
|
||||
(!resolveOption || resolveOption === 'CANCEL') && (
|
||||
<CreateAnswerPanel contract={contract} />
|
||||
)}
|
||||
{outcomeType === 'FREE_RESPONSE' && tradingAllowed(contract) && (
|
||||
<CreateAnswerPanel contract={contract} />
|
||||
)}
|
||||
|
||||
{(user?.id === creatorId || (isAdmin && needsAdminToResolve(contract))) &&
|
||||
!resolution && (
|
||||
|
|
|
@ -48,6 +48,7 @@ export const SORTS = [
|
|||
{ label: 'Daily trending', value: 'daily-score' },
|
||||
{ label: '24h volume', value: '24-hour-vol' },
|
||||
{ label: 'Most popular', value: 'most-popular' },
|
||||
{ label: 'Liquidity', value: 'liquidity' },
|
||||
{ label: 'Last updated', value: 'last-updated' },
|
||||
{ label: 'Closing soon', value: 'close-date' },
|
||||
{ label: 'Resolve date', value: 'resolve-date' },
|
||||
|
|
|
@ -37,12 +37,12 @@ export function ProbChangeTable(props: {
|
|||
|
||||
return (
|
||||
<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) => (
|
||||
<ContractCardProbChange key={contract.id} contract={contract} />
|
||||
))}
|
||||
</Col>
|
||||
<Col className="flex-1 gap-4">
|
||||
<Col className="flex-1">
|
||||
{filteredNegativeChanges.map((contract) => (
|
||||
<ContractCardProbChange key={contract.id} contract={contract} />
|
||||
))}
|
||||
|
|
|
@ -64,11 +64,11 @@ export function BetStatusText(props: {
|
|||
}, [challengeSlug, contract.id])
|
||||
|
||||
const bought = amount >= 0 ? 'bought' : 'sold'
|
||||
const money = formatMoney(Math.abs(amount))
|
||||
const outOfTotalAmount =
|
||||
bet.limitProb !== undefined && bet.orderAmount !== undefined
|
||||
? ` / ${formatMoney(bet.orderAmount)}`
|
||||
? ` of ${bet.isCancelled ? money : formatMoney(bet.orderAmount)}`
|
||||
: ''
|
||||
const money = formatMoney(Math.abs(amount))
|
||||
|
||||
const hadPoolMatch =
|
||||
(bet.limitProb === undefined ||
|
||||
|
@ -105,7 +105,6 @@ export function BetStatusText(props: {
|
|||
{!hideOutcome && (
|
||||
<>
|
||||
{' '}
|
||||
of{' '}
|
||||
<OutcomeLabel
|
||||
outcome={outcome}
|
||||
value={(bet as any).value}
|
||||
|
|
|
@ -145,8 +145,6 @@ function GroupOverviewPinned(props: {
|
|||
}) {
|
||||
const { group, posts, isEditable } = props
|
||||
const [pinned, setPinned] = useState<JSX.Element[]>([])
|
||||
const [open, setOpen] = useState(false)
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
async function getPinned() {
|
||||
|
@ -185,100 +183,127 @@ function GroupOverviewPinned(props: {
|
|||
...(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) ? (
|
||||
pinned.length > 0 || isEditable ? (
|
||||
<div>
|
||||
<Row className="mb-3 items-center justify-between">
|
||||
<SectionHeader label={'Pinned'} />
|
||||
{isEditable && (
|
||||
<Button
|
||||
color="gray"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setEditMode(!editMode)
|
||||
}}
|
||||
>
|
||||
{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}
|
||||
<PinnedItems
|
||||
posts={posts}
|
||||
group={group}
|
||||
isEditable={isEditable}
|
||||
pinned={pinned}
|
||||
onDeleteClicked={onDeleteClicked}
|
||||
onSubmit={onSubmit}
|
||||
modalMessage={'Pin posts or markets to the overview of this group.'}
|
||||
/>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
)
|
||||
}
|
||||
|
||||
{editMode && (
|
||||
<CrossIcon
|
||||
onClick={() => {
|
||||
const newPinned = group.pinnedItems.filter((item) => {
|
||||
return item.itemId !== group.pinnedItems[index].itemId
|
||||
})
|
||||
updateGroup(group, { pinnedItems: newPinned })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{editMode && group.pinnedItems && 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>
|
||||
export function PinnedItems(props: {
|
||||
posts: Post[]
|
||||
isEditable: boolean
|
||||
pinned: JSX.Element[]
|
||||
onDeleteClicked: (index: number) => void
|
||||
onSubmit: (selectedItems: { itemId: string; type: string }[]) => void
|
||||
group?: Group
|
||||
modalMessage: string
|
||||
}) {
|
||||
const {
|
||||
isEditable,
|
||||
pinned,
|
||||
onDeleteClicked,
|
||||
onSubmit,
|
||||
posts,
|
||||
group,
|
||||
modalMessage,
|
||||
} = props
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return pinned.length > 0 || isEditable ? (
|
||||
<div>
|
||||
<Row className="mb-3 items-center justify-between">
|
||||
<SectionHeader label={'Pinned'} />
|
||||
{isEditable && (
|
||||
<Button
|
||||
color="gray"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setEditMode(!editMode)
|
||||
}}
|
||||
>
|
||||
{editMode ? (
|
||||
'Done'
|
||||
) : (
|
||||
<>
|
||||
<PencilIcon className="inline h-4 w-4" />
|
||||
Edit
|
||||
</>
|
||||
)}
|
||||
</Masonry>
|
||||
</div>
|
||||
<PinnedSelectModal
|
||||
open={open}
|
||||
group={group}
|
||||
posts={posts}
|
||||
setOpen={setOpen}
|
||||
title="Pin a post or market"
|
||||
description={
|
||||
<div className={'text-md my-4 text-gray-600'}>
|
||||
Pin posts or markets to the overview of this group.
|
||||
</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>
|
||||
}
|
||||
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>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
)
|
||||
<PinnedSelectModal
|
||||
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'}
|
||||
width={45}
|
||||
height={45}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
{!hideText &&
|
||||
|
|
|
@ -11,13 +11,13 @@ function SidebarButton(props: {
|
|||
}) {
|
||||
const { text, children } = props
|
||||
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
|
||||
className="-ml-1 mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="truncate">{text}</span>
|
||||
{children}
|
||||
</a>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ export function PinnedSelectModal(props: {
|
|||
selectedItems: { itemId: string; type: string }[]
|
||||
) => void | Promise<void>
|
||||
contractSearchOptions?: Partial<Parameters<typeof ContractSearch>[0]>
|
||||
group: Group
|
||||
posts: Post[]
|
||||
group?: Group
|
||||
}) {
|
||||
const {
|
||||
title,
|
||||
|
@ -134,8 +134,8 @@ export function PinnedSelectModal(props: {
|
|||
highlightClassName:
|
||||
'!bg-indigo-100 outline outline-2 outline-indigo-300',
|
||||
}}
|
||||
additionalFilter={{ groupSlug: group.slug }}
|
||||
persistPrefix={`group-${group.slug}`}
|
||||
additionalFilter={group ? { groupSlug: group.slug } : undefined}
|
||||
persistPrefix={group ? `group-${group.slug}` : undefined}
|
||||
headerClassName="bg-white sticky"
|
||||
{...contractSearchOptions}
|
||||
/>
|
||||
|
@ -152,7 +152,7 @@ export function PinnedSelectModal(props: {
|
|||
'!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>
|
||||
|
|
|
@ -34,6 +34,17 @@ export function UserLink(props: {
|
|||
)}
|
||||
>
|
||||
{shortName}
|
||||
{BOT_USERNAMES.includes(username) && <BotBadge />}
|
||||
</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 { Post } from 'common/post'
|
||||
import { listenForPost } from 'web/lib/firebase/posts'
|
||||
import { DateDoc, Post } from 'common/post'
|
||||
import { listenForDateDocs, listenForPost } from 'web/lib/firebase/posts'
|
||||
|
||||
export const usePost = (postId: string | undefined) => {
|
||||
const [post, setPost] = useState<Post | null | undefined>()
|
||||
|
@ -37,3 +37,13 @@ export const usePosts = (postIds: string[]) => {
|
|||
)
|
||||
.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,
|
||||
} from 'firebase/firestore'
|
||||
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'
|
||||
|
||||
export const posts = coll<Post>('posts')
|
||||
|
@ -51,6 +57,11 @@ export async function getDateDocs() {
|
|||
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) {
|
||||
const user = await getUserByUsername(username)
|
||||
if (!user) return null
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
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 {
|
||||
;(function (l, e, a, p) {
|
||||
if (window.Sprig) return
|
||||
|
@ -20,7 +20,8 @@ if (ENV_CONFIG.domain === PROD_CONFIG.domain) {
|
|||
a.async = 1
|
||||
a.src = e + '?id=' + S.appId
|
||||
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)
|
||||
} catch (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"
|
||||
key="image2"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
<AuthProvider serverUser={pageProps.auth}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ENV_CONFIG } from 'common/envs/constants'
|
|||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html data-theme="mantic" className="min-h-screen">
|
||||
<Html lang="en" data-theme="mantic" className="min-h-screen">
|
||||
<Head>
|
||||
<link rel="icon" href={ENV_CONFIG.faviconPath} />
|
||||
<link
|
||||
|
|
|
@ -22,6 +22,7 @@ import { PostCommentsActivity, RichEditPost } from '../post/[...slugs]'
|
|||
import { usePost } from 'web/hooks/use-post'
|
||||
import { useTipTxns } from 'web/hooks/use-tip-txns'
|
||||
import { useCommentsOnPost } from 'web/hooks/use-comments'
|
||||
import { NoSEO } from 'web/components/NoSEO'
|
||||
|
||||
export async function getStaticProps(props: { params: { username: string } }) {
|
||||
const { username } = props.params
|
||||
|
@ -62,6 +63,7 @@ function DateDocPage(props: { creator: User; post: DateDoc }) {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<NoSEO />
|
||||
<Col className="mx-auto w-full max-w-xl gap-6 sm:mb-6">
|
||||
<SiteLink href="/date-docs">
|
||||
<Row className="items-center gap-2">
|
||||
|
@ -140,15 +142,17 @@ export function DateDocPost(props: {
|
|||
) : (
|
||||
<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">
|
||||
<iframe
|
||||
height="405"
|
||||
src={marketUrl}
|
||||
title=""
|
||||
frameBorder="0"
|
||||
className="w-full rounded-xl bg-white p-10"
|
||||
></iframe>
|
||||
</div>
|
||||
{contractSlug && (
|
||||
<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">
|
||||
<iframe
|
||||
height="405"
|
||||
src={marketUrl}
|
||||
title=""
|
||||
frameBorder="0"
|
||||
className="w-full rounded-xl bg-white p-10"
|
||||
></iframe>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ import dayjs from 'dayjs'
|
|||
import { MINUTE_MS } from 'common/util/time'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
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() {
|
||||
const user = useUser()
|
||||
|
@ -25,6 +28,7 @@ export default function CreateDateDocPage() {
|
|||
const title = `${user?.name}'s Date Doc`
|
||||
const subtitle = 'Manifold dating docs'
|
||||
const [birthday, setBirthday] = useState<undefined | string>(undefined)
|
||||
const [createMarket, setCreateMarket] = useState(true)
|
||||
const [question, setQuestion] = useState(
|
||||
'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 isValid =
|
||||
user && birthday && editor && editor.isEmpty === false && question
|
||||
user &&
|
||||
birthday &&
|
||||
editor &&
|
||||
editor.isEmpty === false &&
|
||||
(question || !createMarket)
|
||||
|
||||
async function saveDateDoc() {
|
||||
if (!user || !editor || !birthdayTime) return
|
||||
|
@ -45,15 +53,15 @@ export default function CreateDateDocPage() {
|
|||
const newPost: Omit<
|
||||
DateDoc,
|
||||
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
|
||||
> & { question: string } = {
|
||||
> & { question?: string } = removeUndefinedProps({
|
||||
title,
|
||||
subtitle,
|
||||
content: editor.getJSON(),
|
||||
bounty: 0,
|
||||
birthday: birthdayTime,
|
||||
type: 'date-doc',
|
||||
question,
|
||||
}
|
||||
question: createMarket ? question : undefined,
|
||||
})
|
||||
|
||||
const result = await createPost(newPost)
|
||||
|
||||
|
@ -64,6 +72,7 @@ export default function CreateDateDocPage() {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<NoSEO />
|
||||
<div className="mx-auto w-full max-w-3xl">
|
||||
<div className="rounded-lg px-6 py-4 pb-4 sm:py-0">
|
||||
<Row className="mb-8 items-center justify-between">
|
||||
|
@ -104,9 +113,13 @@ export default function CreateDateDocPage() {
|
|||
</Col>
|
||||
|
||||
<Col className="gap-4">
|
||||
<div className="">
|
||||
Finally, we'll create an (unlisted) prediction market!
|
||||
</div>
|
||||
<Row className="items-center gap-4">
|
||||
<ShortToggle
|
||||
on={createMarket}
|
||||
setOn={(on) => setCreateMarket(on)}
|
||||
/>
|
||||
Create an (unlisted) prediction market attached to the date doc
|
||||
</Row>
|
||||
|
||||
<Col className="gap-2">
|
||||
<Textarea
|
||||
|
@ -114,6 +127,7 @@ export default function CreateDateDocPage() {
|
|||
maxLength={MAX_QUESTION_LENGTH}
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value || '')}
|
||||
disabled={!createMarket}
|
||||
/>
|
||||
<div className="ml-2 text-gray-500">Cost: M$100</div>
|
||||
</Col>
|
||||
|
|
|
@ -12,6 +12,8 @@ import { Button } from 'web/components/button'
|
|||
import { SiteLink } from 'web/components/site-link'
|
||||
import { getUser, User } from 'web/lib/firebase/users'
|
||||
import { DateDocPost } from './[username]'
|
||||
import { NoSEO } from 'web/components/NoSEO'
|
||||
import { useDateDocs } from 'web/hooks/use-post'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const dateDocs = await getDateDocs()
|
||||
|
@ -33,13 +35,15 @@ export default function DatePage(props: {
|
|||
dateDocs: DateDoc[]
|
||||
docCreators: User[]
|
||||
}) {
|
||||
const { dateDocs, docCreators } = props
|
||||
const { docCreators } = props
|
||||
const user = useUser()
|
||||
|
||||
const dateDocs = useDateDocs() ?? props.dateDocs
|
||||
const hasDoc = dateDocs.some((d) => d.creatorId === user?.id)
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<NoSEO />
|
||||
<div className="mx-auto w-full max-w-xl">
|
||||
<Row className="items-center justify-between p-4 sm:p-0">
|
||||
<Title className="!my-0 px-2 text-blue-500" text="Date docs" />
|
||||
|
|
Loading…
Reference in New Issue
Block a user