Merge branch 'main' into theoremone
This commit is contained in:
commit
5d64d53c65
|
@ -4,6 +4,7 @@ export type Bet = {
|
||||||
contractId: string
|
contractId: string
|
||||||
|
|
||||||
amount: number // bet size; negative if SELL bet
|
amount: number // bet size; negative if SELL bet
|
||||||
|
loanAmount?: number
|
||||||
outcome: string
|
outcome: string
|
||||||
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
||||||
|
|
||||||
|
@ -21,3 +22,5 @@ export type Bet = {
|
||||||
|
|
||||||
createdTime: number
|
createdTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAX_LOAN_PER_CONTRACT = 20
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Bet } from './bet'
|
import * as _ from 'lodash'
|
||||||
|
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateShares,
|
calculateShares,
|
||||||
getProbability,
|
getProbability,
|
||||||
|
@ -11,6 +12,7 @@ export const getNewBinaryBetInfo = (
|
||||||
user: User,
|
user: User,
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
amount: number,
|
amount: number,
|
||||||
|
loanAmount: number,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
|
@ -45,6 +47,7 @@ export const getNewBinaryBetInfo = (
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
|
loanAmount,
|
||||||
shares,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
|
@ -52,7 +55,7 @@ export const getNewBinaryBetInfo = (
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - amount
|
const newBalance = user.balance - (amount - loanAmount)
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||||
}
|
}
|
||||||
|
@ -61,6 +64,7 @@ export const getNewMultiBetInfo = (
|
||||||
user: User,
|
user: User,
|
||||||
outcome: string,
|
outcome: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
|
loanAmount: number,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
|
@ -85,6 +89,7 @@ export const getNewMultiBetInfo = (
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
|
loanAmount,
|
||||||
shares,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
|
@ -92,7 +97,17 @@ export const getNewMultiBetInfo = (
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - amount
|
const newBalance = user.balance - (amount - loanAmount)
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
|
||||||
|
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
|
const prevLoanAmount = _.sumBy(openBets, (bet) => bet.loanAmount ?? 0)
|
||||||
|
const loanAmount = Math.min(
|
||||||
|
newBetAmount,
|
||||||
|
MAX_LOAN_PER_CONTRACT - prevLoanAmount
|
||||||
|
)
|
||||||
|
return loanAmount
|
||||||
|
}
|
||||||
|
|
|
@ -138,8 +138,7 @@ export const getPayoutsMultiOutcome = (
|
||||||
const prob = resolutions[outcome] / probTotal
|
const prob = resolutions[outcome] / probTotal
|
||||||
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
|
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
|
||||||
const profit = winnings - amount
|
const profit = winnings - amount
|
||||||
|
const payout = deductFees(amount, winnings)
|
||||||
const payout = amount + (1 - FEES) * Math.max(0, profit)
|
|
||||||
return { userId, profit, payout }
|
return { userId, profit, payout }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -161,3 +160,12 @@ export const getPayoutsMultiOutcome = (
|
||||||
.map(({ userId, payout }) => ({ userId, payout }))
|
.map(({ userId, payout }) => ({ userId, payout }))
|
||||||
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
|
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getLoanPayouts = (bets: Bet[]) => {
|
||||||
|
const betsWithLoans = bets.filter((bet) => bet.loanAmount)
|
||||||
|
const betsByUser = _.groupBy(betsWithLoans, (bet) => bet.userId)
|
||||||
|
const loansByUser = _.mapValues(betsByUser, (bets) =>
|
||||||
|
_.sumBy(bets, (bet) => -(bet.loanAmount ?? 0))
|
||||||
|
)
|
||||||
|
return _.toPairs(loansByUser).map(([userId, payout]) => ({ userId, payout }))
|
||||||
|
}
|
||||||
|
|
|
@ -45,8 +45,9 @@ export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||||
const investments = bets
|
const investments = bets
|
||||||
.filter((bet) => !bet.sale)
|
.filter((bet) => !bet.sale)
|
||||||
.map((bet) => {
|
.map((bet) => {
|
||||||
const { userId, amount } = bet
|
const { userId, amount, loanAmount } = bet
|
||||||
return { userId, payout: -amount }
|
const payout = -amount - (loanAmount ?? 0)
|
||||||
|
return { userId, payout }
|
||||||
})
|
})
|
||||||
|
|
||||||
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
|
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const getSellBetInfo = (
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
const { pool, totalShares, totalBets } = contract
|
const { pool, totalShares, totalBets } = contract
|
||||||
const { id: betId, amount, shares, outcome } = bet
|
const { id: betId, amount, shares, outcome, loanAmount } = bet
|
||||||
|
|
||||||
const adjShareValue = calculateShareValue(contract, bet)
|
const adjShareValue = calculateShareValue(contract, bet)
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ export const getSellBetInfo = (
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance + saleAmount
|
const newBalance = user.balance + saleAmount - (loanAmount ?? 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newBet,
|
newBet,
|
||||||
|
|
|
@ -3,10 +3,11 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { getNewMultiBetInfo } from '../../common/new-bet'
|
import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
import { getContract, getValues } from './utils'
|
import { getContract, getValues } from './utils'
|
||||||
import { sendNewAnswerEmail } from './emails'
|
import { sendNewAnswerEmail } from './emails'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
|
||||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -55,6 +56,11 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
return { status: 'error', message: 'Trading is closed' }
|
||||||
|
|
||||||
|
const yourBetsSnap = await transaction.get(
|
||||||
|
contractDoc.collection('bets').where('userId', '==', userId)
|
||||||
|
)
|
||||||
|
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
||||||
|
|
||||||
const [lastAnswer] = await getValues<Answer>(
|
const [lastAnswer] = await getValues<Answer>(
|
||||||
firestore
|
firestore
|
||||||
.collection(`contracts/${contractId}/answers`)
|
.collection(`contracts/${contractId}/answers`)
|
||||||
|
@ -92,8 +98,17 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
.collection(`contracts/${contractId}/bets`)
|
.collection(`contracts/${contractId}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
|
const loanAmount = getLoanAmount(yourBets, amount)
|
||||||
|
|
||||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||||
getNewMultiBetInfo(user, answerId, amount, contract, newBetDoc.id)
|
getNewMultiBetInfo(
|
||||||
|
user,
|
||||||
|
answerId,
|
||||||
|
amount,
|
||||||
|
loanAmount,
|
||||||
|
contract,
|
||||||
|
newBetDoc.id
|
||||||
|
)
|
||||||
|
|
||||||
transaction.create(newBetDoc, newBet)
|
transaction.create(newBetDoc, newBet)
|
||||||
transaction.update(contractDoc, {
|
transaction.update(contractDoc, {
|
||||||
|
|
|
@ -223,6 +223,10 @@ export const sendNewAnswerEmail = async (
|
||||||
) => {
|
) => {
|
||||||
// Send to just the creator for now.
|
// Send to just the creator for now.
|
||||||
const { creatorId: userId } = contract
|
const { creatorId: userId } = contract
|
||||||
|
|
||||||
|
// Don't send the creator's own answers.
|
||||||
|
if (answer.userId === userId) return
|
||||||
|
|
||||||
const privateUser = await getPrivateUser(userId)
|
const privateUser = await getPrivateUser(userId)
|
||||||
if (
|
if (
|
||||||
!privateUser ||
|
!privateUser ||
|
||||||
|
|
|
@ -3,7 +3,12 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { getNewBinaryBetInfo, getNewMultiBetInfo } from '../../common/new-bet'
|
import {
|
||||||
|
getLoanAmount,
|
||||||
|
getNewBinaryBetInfo,
|
||||||
|
getNewMultiBetInfo,
|
||||||
|
} from '../../common/new-bet'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
|
||||||
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -33,9 +38,6 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
return { status: 'error', message: 'User not found' }
|
return { status: 'error', message: 'User not found' }
|
||||||
const user = userSnap.data() as User
|
const user = userSnap.data() as User
|
||||||
|
|
||||||
if (user.balance < amount)
|
|
||||||
return { status: 'error', message: 'Insufficient balance' }
|
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const contractSnap = await transaction.get(contractDoc)
|
const contractSnap = await transaction.get(contractDoc)
|
||||||
if (!contractSnap.exists)
|
if (!contractSnap.exists)
|
||||||
|
@ -46,6 +48,15 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
return { status: 'error', message: 'Trading is closed' }
|
||||||
|
|
||||||
|
const yourBetsSnap = await transaction.get(
|
||||||
|
contractDoc.collection('bets').where('userId', '==', userId)
|
||||||
|
)
|
||||||
|
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
||||||
|
|
||||||
|
const loanAmount = getLoanAmount(yourBets, amount)
|
||||||
|
if (user.balance < amount - loanAmount)
|
||||||
|
return { status: 'error', message: 'Insufficient balance' }
|
||||||
|
|
||||||
if (outcomeType === 'FREE_RESPONSE') {
|
if (outcomeType === 'FREE_RESPONSE') {
|
||||||
const answerSnap = await transaction.get(
|
const answerSnap = await transaction.get(
|
||||||
contractDoc.collection('answers').doc(outcome)
|
contractDoc.collection('answers').doc(outcome)
|
||||||
|
@ -64,10 +75,18 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
user,
|
user,
|
||||||
outcome as 'YES' | 'NO',
|
outcome as 'YES' | 'NO',
|
||||||
amount,
|
amount,
|
||||||
|
loanAmount,
|
||||||
|
contract,
|
||||||
|
newBetDoc.id
|
||||||
|
)
|
||||||
|
: getNewMultiBetInfo(
|
||||||
|
user,
|
||||||
|
outcome,
|
||||||
|
amount,
|
||||||
|
loanAmount,
|
||||||
contract,
|
contract,
|
||||||
newBetDoc.id
|
newBetDoc.id
|
||||||
)
|
)
|
||||||
: getNewMultiBetInfo(user, outcome, amount, contract, newBetDoc.id)
|
|
||||||
|
|
||||||
transaction.create(newBetDoc, newBet)
|
transaction.create(newBetDoc, newBet)
|
||||||
transaction.update(contractDoc, {
|
transaction.update(contractDoc, {
|
||||||
|
|
|
@ -7,7 +7,11 @@ import { User } from '../../common/user'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getUser, payUser } from './utils'
|
import { getUser, payUser } from './utils'
|
||||||
import { sendMarketResolutionEmail } from './emails'
|
import { sendMarketResolutionEmail } from './emails'
|
||||||
import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts'
|
import {
|
||||||
|
getLoanPayouts,
|
||||||
|
getPayouts,
|
||||||
|
getPayoutsMultiOutcome,
|
||||||
|
} from '../../common/payouts'
|
||||||
import { removeUndefinedProps } from '../../common/util/object'
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
|
|
||||||
export const resolveMarket = functions
|
export const resolveMarket = functions
|
||||||
|
@ -99,13 +103,23 @@ export const resolveMarket = functions
|
||||||
? getPayoutsMultiOutcome(resolutions, contract, openBets)
|
? getPayoutsMultiOutcome(resolutions, contract, openBets)
|
||||||
: getPayouts(outcome, contract, openBets, resolutionProbability)
|
: getPayouts(outcome, contract, openBets, resolutionProbability)
|
||||||
|
|
||||||
|
const loanPayouts = getLoanPayouts(openBets)
|
||||||
|
|
||||||
console.log('payouts:', payouts)
|
console.log('payouts:', payouts)
|
||||||
|
|
||||||
const groups = _.groupBy(payouts, (payout) => payout.userId)
|
const groups = _.groupBy(
|
||||||
|
[...payouts, ...loanPayouts],
|
||||||
|
(payout) => payout.userId
|
||||||
|
)
|
||||||
const userPayouts = _.mapValues(groups, (group) =>
|
const userPayouts = _.mapValues(groups, (group) =>
|
||||||
_.sumBy(group, (g) => g.payout)
|
_.sumBy(group, (g) => g.payout)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const groupsWithoutLoans = _.groupBy(payouts, (payout) => payout.userId)
|
||||||
|
const userPayoutsWithoutLoans = _.mapValues(groupsWithoutLoans, (group) =>
|
||||||
|
_.sumBy(group, (g) => g.payout)
|
||||||
|
)
|
||||||
|
|
||||||
const payoutPromises = Object.entries(userPayouts).map(
|
const payoutPromises = Object.entries(userPayouts).map(
|
||||||
([userId, payout]) => payUser(userId, payout)
|
([userId, payout]) => payUser(userId, payout)
|
||||||
)
|
)
|
||||||
|
@ -116,7 +130,7 @@ export const resolveMarket = functions
|
||||||
|
|
||||||
await sendResolutionEmails(
|
await sendResolutionEmails(
|
||||||
openBets,
|
openBets,
|
||||||
userPayouts,
|
userPayoutsWithoutLoans,
|
||||||
creator,
|
creator,
|
||||||
contract,
|
contract,
|
||||||
outcome,
|
outcome,
|
||||||
|
|
|
@ -52,7 +52,8 @@ const computeInvestmentValue = async (
|
||||||
if (!contract || contract.isResolved) return 0
|
if (!contract || contract.isResolved) return 0
|
||||||
if (bet.sale || bet.isSold) return 0
|
if (bet.sale || bet.isSold) return 0
|
||||||
|
|
||||||
return calculatePayout(contract, bet, 'MKT')
|
const payout = calculatePayout(contract, bet, 'MKT')
|
||||||
|
return payout - (bet.loanAmount ?? 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,8 +68,7 @@ const updateUserBalance = (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const payUser = (userId: string, payout: number, isDeposit = false) => {
|
export const payUser = (userId: string, payout: number, isDeposit = false) => {
|
||||||
if (!isFinite(payout) || payout <= 0)
|
if (!isFinite(payout)) throw new Error('Payout is not finite: ' + payout)
|
||||||
throw new Error('Payout is not positive: ' + payout)
|
|
||||||
|
|
||||||
return updateUserBalance(userId, payout, isDeposit)
|
return updateUserBalance(userId, payout, isDeposit)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { ContractFeed, ContractSummaryFeed } from '../components/contract-feed'
|
import {
|
||||||
import { Page } from '../components/page'
|
ContractActivityFeed,
|
||||||
|
ContractFeed,
|
||||||
|
ContractSummaryFeed,
|
||||||
|
} from './contract-feed'
|
||||||
import { Contract } from '../lib/firebase/contracts'
|
import { Contract } from '../lib/firebase/contracts'
|
||||||
import { Comment } from '../lib/firebase/comments'
|
import { Comment } from '../lib/firebase/comments'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
|
|
||||||
const MAX_ACTIVE_CONTRACTS = 75
|
const MAX_ACTIVE_CONTRACTS = 75
|
||||||
|
@ -72,30 +75,44 @@ export function findActiveContracts(
|
||||||
|
|
||||||
export function ActivityFeed(props: {
|
export function ActivityFeed(props: {
|
||||||
contracts: Contract[]
|
contracts: Contract[]
|
||||||
contractBets: Bet[][]
|
recentBets: Bet[]
|
||||||
contractComments: Comment[][]
|
recentComments: Comment[]
|
||||||
|
loadBetAndCommentHistory?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { contracts, contractBets, contractComments } = props
|
const { contracts, recentBets, recentComments, loadBetAndCommentHistory } =
|
||||||
|
props
|
||||||
|
|
||||||
return contracts.length > 0 ? (
|
const groupedBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||||
|
const groupedComments = _.groupBy(
|
||||||
|
recentComments,
|
||||||
|
(comment) => comment.contractId
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="w-full max-w-3xl">
|
<Col className="w-full">
|
||||||
<Col className="w-full divide-y divide-gray-300 self-center bg-white">
|
<Col className="w-full divide-y divide-gray-300 self-center bg-white">
|
||||||
{contracts.map((contract, i) => (
|
{contracts.map((contract) => (
|
||||||
<div key={contract.id} className="py-6 px-2 sm:px-4">
|
<div key={contract.id} className="py-6 px-2 sm:px-4">
|
||||||
<ContractFeed
|
{loadBetAndCommentHistory ? (
|
||||||
contract={contract}
|
<ContractFeed
|
||||||
bets={contractBets[i]}
|
contract={contract}
|
||||||
comments={contractComments[i]}
|
bets={groupedBets[contract.id] ?? []}
|
||||||
feedType="activity"
|
comments={groupedComments[contract.id] ?? []}
|
||||||
/>
|
feedType="activity"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ContractActivityFeed
|
||||||
|
contract={contract}
|
||||||
|
bets={groupedBets[contract.id] ?? []}
|
||||||
|
comments={groupedComments[contract.id] ?? []}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,11 +133,3 @@ export function SummaryActivityFeed(props: { contracts: Contract[] }) {
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ActivityPage() {
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<ActivityFeed contracts={[]} contractBets={[]} contractComments={[]} />
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,15 +1,20 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import _ from 'lodash'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { formatMoney } from '../../common/util/format'
|
import { formatMoney } from '../../common/util/format'
|
||||||
import { AddFundsButton } from './add-funds-button'
|
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
|
import { useUserContractBets } from '../hooks/use-user-bets'
|
||||||
|
import { MAX_LOAN_PER_CONTRACT } from '../../common/bet'
|
||||||
|
import { InfoTooltip } from './info-tooltip'
|
||||||
|
import { Spacer } from './layout/spacer'
|
||||||
|
|
||||||
export function AmountInput(props: {
|
export function AmountInput(props: {
|
||||||
amount: number | undefined
|
amount: number | undefined
|
||||||
onChange: (newAmount: number | undefined) => void
|
onChange: (newAmount: number | undefined) => void
|
||||||
error: string | undefined
|
error: string | undefined
|
||||||
setError: (error: string | undefined) => void
|
setError: (error: string | undefined) => void
|
||||||
|
contractIdForLoan: string | undefined
|
||||||
minimumAmount?: number
|
minimumAmount?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
@ -22,6 +27,7 @@ export function AmountInput(props: {
|
||||||
onChange,
|
onChange,
|
||||||
error,
|
error,
|
||||||
setError,
|
setError,
|
||||||
|
contractIdForLoan,
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
|
@ -31,14 +37,32 @@ export function AmountInput(props: {
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
|
const userBets = useUserContractBets(user?.id, contractIdForLoan) ?? []
|
||||||
|
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
|
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
|
||||||
|
|
||||||
|
const loanAmount = contractIdForLoan
|
||||||
|
? Math.min(amount ?? 0, MAX_LOAN_PER_CONTRACT - prevLoanAmount)
|
||||||
|
: 0
|
||||||
|
|
||||||
const onAmountChange = (str: string) => {
|
const onAmountChange = (str: string) => {
|
||||||
|
if (str.includes('-')) {
|
||||||
|
onChange(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
const amount = parseInt(str.replace(/[^\d]/, ''))
|
const amount = parseInt(str.replace(/[^\d]/, ''))
|
||||||
|
|
||||||
if (str && isNaN(amount)) return
|
if (str && isNaN(amount)) return
|
||||||
|
if (amount >= 10 ** 9) return
|
||||||
|
|
||||||
onChange(str ? amount : undefined)
|
onChange(str ? amount : undefined)
|
||||||
|
|
||||||
if (user && user.balance < amount) {
|
const loanAmount = contractIdForLoan
|
||||||
|
? Math.min(amount, MAX_LOAN_PER_CONTRACT - prevLoanAmount)
|
||||||
|
: 0
|
||||||
|
const amountNetLoan = amount - loanAmount
|
||||||
|
|
||||||
|
if (user && user.balance < amountNetLoan) {
|
||||||
setError('Insufficient balance')
|
setError('Insufficient balance')
|
||||||
} else if (minimumAmount && amount < minimumAmount) {
|
} else if (minimumAmount && amount < minimumAmount) {
|
||||||
setError('Minimum amount: ' + formatMoney(minimumAmount))
|
setError('Minimum amount: ' + formatMoney(minimumAmount))
|
||||||
|
@ -47,7 +71,8 @@ export function AmountInput(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainingBalance = Math.max(0, (user?.balance ?? 0) - (amount ?? 0))
|
const amountNetLoan = (amount ?? 0) - loanAmount
|
||||||
|
const remainingBalance = Math.max(0, (user?.balance ?? 0) - amountNetLoan)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
|
@ -68,19 +93,34 @@ export function AmountInput(props: {
|
||||||
onChange={(e) => onAmountChange(e.target.value)}
|
onChange={(e) => onAmountChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mr-auto mt-4 self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
|
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{user && (
|
{user && (
|
||||||
<Col className="mt-3 text-sm">
|
<Col className="gap-3 text-sm">
|
||||||
<div className="mb-2 whitespace-nowrap text-gray-500">
|
{contractIdForLoan && (
|
||||||
Remaining balance
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
</div>
|
<Row className="items-center gap-2">
|
||||||
<Row className="gap-4">
|
Amount loaned{' '}
|
||||||
<div>{formatMoney(Math.floor(remainingBalance))}</div>
|
<InfoTooltip
|
||||||
{user.balance !== 1000 && <AddFundsButton />}
|
text={`In every market, you get an interest-free loan on the first ${formatMoney(
|
||||||
|
MAX_LOAN_PER_CONTRACT
|
||||||
|
)}.`}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<span className="text-neutral">{formatMoney(loanAmount)}</span>{' '}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
|
Remaining balance{' '}
|
||||||
|
<span className="text-neutral">
|
||||||
|
{formatMoney(Math.floor(remainingBalance))}
|
||||||
|
</span>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
49
web/components/analytics/charts.tsx
Normal file
49
web/components/analytics/charts.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { ResponsiveLine } from '@nivo/line'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { useWindowSize } from '../../hooks/use-window-size'
|
||||||
|
|
||||||
|
export function DailyCountChart(props: {
|
||||||
|
startDate: number
|
||||||
|
dailyCounts: number[]
|
||||||
|
small?: boolean
|
||||||
|
}) {
|
||||||
|
const { dailyCounts, startDate, small } = props
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
|
||||||
|
const dates = dailyCounts.map((_, i) =>
|
||||||
|
dayjs(startDate).add(i, 'day').toDate()
|
||||||
|
)
|
||||||
|
|
||||||
|
const points = _.zip(dates, dailyCounts).map(([date, betCount]) => ({
|
||||||
|
x: date,
|
||||||
|
y: betCount,
|
||||||
|
}))
|
||||||
|
const data = [{ id: 'Count', data: points, color: '#11b981' }]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full"
|
||||||
|
style={{ height: !small && (!width || width >= 800) ? 400 : 250 }}
|
||||||
|
>
|
||||||
|
<ResponsiveLine
|
||||||
|
data={data}
|
||||||
|
yScale={{ type: 'linear', stacked: false }}
|
||||||
|
xScale={{
|
||||||
|
type: 'time',
|
||||||
|
}}
|
||||||
|
axisBottom={{
|
||||||
|
format: (date) => dayjs(date).format('MMM DD'),
|
||||||
|
}}
|
||||||
|
colors={{ datum: 'color' }}
|
||||||
|
pointSize={width && width >= 800 ? 10 : 0}
|
||||||
|
pointBorderWidth={1}
|
||||||
|
pointBorderColor="#fff"
|
||||||
|
enableSlices="x"
|
||||||
|
enableGridX={!!width && width >= 800}
|
||||||
|
enableArea
|
||||||
|
margin={{ top: 20, right: 28, bottom: 22, left: 40 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -30,8 +30,9 @@ export function AnswerBetPanel(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
contract: Contract
|
contract: Contract
|
||||||
closePanel: () => void
|
closePanel: () => void
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { answer, contract, closePanel } = props
|
const { answer, contract, closePanel, className } = props
|
||||||
const { id: answerId } = answer
|
const { id: answerId } = answer
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -48,11 +49,6 @@ export function AnswerBetPanel(props: {
|
||||||
async function submitBet() {
|
async function submitBet() {
|
||||||
if (!user || !betAmount) return
|
if (!user || !betAmount) return
|
||||||
|
|
||||||
if (user.balance < betAmount) {
|
|
||||||
setError('Insufficient balance')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
@ -97,12 +93,12 @@ export function AnswerBetPanel(props: {
|
||||||
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="items-start px-2 pb-2 pt-4 sm:pt-0">
|
<Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}>
|
||||||
<Row className="self-stretch items-center justify-between">
|
<Row className="items-center justify-between self-stretch">
|
||||||
<div className="text-xl">Buy this answer</div>
|
<div className="text-xl">Buy this answer</div>
|
||||||
|
|
||||||
<button className="btn-ghost btn-circle" onClick={closePanel}>
|
<button className="btn-ghost btn-circle" onClick={closePanel}>
|
||||||
<XIcon className="w-8 h-8 text-gray-500 mx-auto" aria-hidden="true" />
|
<XIcon className="mx-auto h-8 w-8 text-gray-500" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Row>
|
</Row>
|
||||||
<div className="my-3 text-left text-sm text-gray-500">Amount </div>
|
<div className="my-3 text-left text-sm text-gray-500">Amount </div>
|
||||||
|
@ -114,40 +110,44 @@ export function AnswerBetPanel(props: {
|
||||||
setError={setError}
|
setError={setError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
|
contractIdForLoan={contract.id}
|
||||||
/>
|
/>
|
||||||
|
<Col className="mt-3 w-full gap-3">
|
||||||
|
<Row className="items-center justify-between text-sm">
|
||||||
|
<div className="text-gray-500">Probability</div>
|
||||||
|
<Row>
|
||||||
|
<div>{formatPercent(initialProb)}</div>
|
||||||
|
<div className="mx-2">→</div>
|
||||||
|
<div>{formatPercent(resultProb)}</div>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Row className="items-start justify-between gap-2 text-sm">
|
||||||
|
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
||||||
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
|
<div>Payout if chosen</div>
|
||||||
<Row>
|
<InfoTooltip
|
||||||
<div>{formatPercent(initialProb)}</div>
|
text={`Current payout for ${formatWithCommas(
|
||||||
<div className="mx-2">→</div>
|
shares
|
||||||
<div>{formatPercent(resultProb)}</div>
|
)} / ${formatWithCommas(
|
||||||
</Row>
|
shares + contract.totalShares[answerId]
|
||||||
|
)} shares`}
|
||||||
<Spacer h={4} />
|
/>
|
||||||
|
</Row>
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
<Row className="flex-wrap items-end justify-end gap-2">
|
||||||
Payout if chosen
|
<span className="whitespace-nowrap">
|
||||||
<InfoTooltip
|
{formatMoney(currentPayout)}
|
||||||
text={`Current payout for ${formatWithCommas(
|
</span>
|
||||||
shares
|
<span>(+{currentReturnPercent})</span>
|
||||||
)} / ${formatWithCommas(
|
</Row>
|
||||||
shares + contract.totalShares[answerId]
|
</Row>
|
||||||
)} shares`}
|
</Col>
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<div>
|
|
||||||
{formatMoney(currentPayout)}
|
|
||||||
<span>(+{currentReturnPercent})</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn',
|
'btn self-stretch',
|
||||||
betDisabled ? 'btn-disabled' : 'btn-primary',
|
betDisabled ? 'btn-disabled' : 'btn-primary',
|
||||||
isSubmitting ? 'loading' : ''
|
isSubmitting ? 'loading' : ''
|
||||||
)}
|
)}
|
||||||
|
@ -157,7 +157,7 @@ export function AnswerBetPanel(props: {
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
className="btn self-stretch whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||||
onClick={firebaseLogin}
|
onClick={firebaseLogin}
|
||||||
>
|
>
|
||||||
Sign in to trade!
|
Sign in to trade!
|
||||||
|
|
|
@ -97,6 +97,7 @@ export function AnswerItem(props: {
|
||||||
answer={answer}
|
answer={answer}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
closePanel={() => setIsBetting(false)}
|
closePanel={() => setIsBetting(false)}
|
||||||
|
className="sm:w-72"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Row className="items-center justify-end gap-4 self-end sm:self-start">
|
<Row className="items-center justify-end gap-4 self-end sm:self-start">
|
||||||
|
|
|
@ -71,9 +71,9 @@ export function AnswerResolvePanel(props: {
|
||||||
: 'btn-disabled'
|
: 'btn-disabled'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-4 p-4 bg-gray-50 rounded">
|
<Col className="gap-4 rounded bg-gray-50 p-4">
|
||||||
<div>Resolve your market</div>
|
<div>Resolve your market</div>
|
||||||
<Col className="sm:flex-row sm:items-center gap-4">
|
<Col className="gap-4 sm:flex-row sm:items-center">
|
||||||
<ChooseCancelSelector
|
<ChooseCancelSelector
|
||||||
className="sm:!flex-row sm:items-center"
|
className="sm:!flex-row sm:items-center"
|
||||||
selected={resolveOption}
|
selected={resolveOption}
|
||||||
|
|
|
@ -95,9 +95,9 @@ export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) {
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{sortedAnswers.length === 0 ? (
|
{sortedAnswers.length === 0 ? (
|
||||||
<div className="text-gray-500 p-4">No answers yet...</div>
|
<div className="p-4 text-gray-500">No answers yet...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-gray-500 self-end p-4">
|
<div className="self-end p-4 text-gray-500">
|
||||||
None of the above:{' '}
|
None of the above:{' '}
|
||||||
{formatPercent(getOutcomeProbability(contract.totalShares, '0'))}
|
{formatPercent(getOutcomeProbability(contract.totalShares, '0'))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,6 +36,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
||||||
const submitAnswer = async () => {
|
const submitAnswer = async () => {
|
||||||
if (canSubmit) {
|
if (canSubmit) {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const result = await createAnswer({
|
const result = await createAnswer({
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
text,
|
text,
|
||||||
|
@ -48,7 +49,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
||||||
setText('')
|
setText('')
|
||||||
setBetAmount(10)
|
setBetAmount(10)
|
||||||
setAmountError(undefined)
|
setAmountError(undefined)
|
||||||
}
|
} else setAmountError(result.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
||||||
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-4 p-4 bg-gray-50 rounded">
|
<Col className="gap-4 rounded bg-gray-50 p-4">
|
||||||
<Col className="flex-1 gap-2">
|
<Col className="flex-1 gap-2">
|
||||||
<div className="mb-1">Add your answer</div>
|
<div className="mb-1">Add your answer</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
@ -86,14 +87,14 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
||||||
<div />
|
<div />
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'sm:flex-row gap-4',
|
'gap-4 sm:flex-row sm:items-end',
|
||||||
text ? 'justify-between' : 'self-end'
|
text ? 'justify-between' : 'self-end'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{text && (
|
{text && (
|
||||||
<>
|
<>
|
||||||
<Col className="gap-2 mt-1">
|
<Col className="mt-1 gap-2">
|
||||||
<div className="text-gray-500 text-sm">Buy amount</div>
|
<div className="text-sm text-gray-500">Buy amount</div>
|
||||||
<AmountInput
|
<AmountInput
|
||||||
amount={betAmount}
|
amount={betAmount}
|
||||||
onChange={setBetAmount}
|
onChange={setBetAmount}
|
||||||
|
@ -101,34 +102,42 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
||||||
setError={setAmountError}
|
setError={setAmountError}
|
||||||
minimumAmount={1}
|
minimumAmount={1}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
contractIdForLoan={contract.id}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="gap-2 mt-1">
|
<Col className="gap-3">
|
||||||
<div className="text-sm text-gray-500">Implied probability</div>
|
<Row className="items-center justify-between text-sm">
|
||||||
<Row>
|
<div className="text-gray-500">Probability</div>
|
||||||
<div>{formatPercent(0)}</div>
|
<Row>
|
||||||
<div className="mx-2">→</div>
|
<div>{formatPercent(0)}</div>
|
||||||
<div>{formatPercent(resultProb)}</div>
|
<div className="mx-2">→</div>
|
||||||
|
<div>{formatPercent(resultProb)}</div>
|
||||||
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
|
||||||
Payout if chosen
|
<Row className="justify-between gap-2 text-sm">
|
||||||
<InfoTooltip
|
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
||||||
text={`Current payout for ${formatWithCommas(
|
<div>Payout if chosen</div>
|
||||||
shares
|
<InfoTooltip
|
||||||
)} / ${formatWithCommas(shares)} shares`}
|
text={`Current payout for ${formatWithCommas(
|
||||||
/>
|
shares
|
||||||
|
)} / ${formatWithCommas(shares)} shares`}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row className="flex-wrap items-end justify-end gap-2">
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
|
{formatMoney(currentPayout)}
|
||||||
|
</span>
|
||||||
|
<span>(+{currentReturnPercent})</span>
|
||||||
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<div>
|
|
||||||
{formatMoney(currentPayout)}
|
|
||||||
<span>(+{currentReturnPercent})</span>
|
|
||||||
</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{user ? (
|
{user ? (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn self-end mt-2',
|
'btn mt-2',
|
||||||
canSubmit ? 'btn-outline' : 'btn-disabled',
|
canSubmit ? 'btn-outline' : 'btn-disabled',
|
||||||
isSubmitting && 'loading'
|
isSubmitting && 'loading'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -78,11 +78,6 @@ export function BetPanel(props: {
|
||||||
async function submitBet() {
|
async function submitBet() {
|
||||||
if (!user || !betAmount) return
|
if (!user || !betAmount) return
|
||||||
|
|
||||||
if (user.balance < betAmount) {
|
|
||||||
setError('Insufficient balance')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
@ -144,7 +139,6 @@ export function BetPanel(props: {
|
||||||
text={panelTitle}
|
text={panelTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <div className="mt-2 mb-1 text-sm text-gray-500">Outcome</div> */}
|
|
||||||
<YesNoSelector
|
<YesNoSelector
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
selected={betChoice}
|
selected={betChoice}
|
||||||
|
@ -160,45 +154,49 @@ export function BetPanel(props: {
|
||||||
setError={setError}
|
setError={setError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
|
contractIdForLoan={contract.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Col className="mt-3 w-full gap-3">
|
||||||
|
<Row className="items-center justify-between text-sm">
|
||||||
|
<div className="text-gray-500">Probability</div>
|
||||||
|
<Row>
|
||||||
|
<div>{formatPercent(initialProb)}</div>
|
||||||
|
<div className="mx-2">→</div>
|
||||||
|
<div>{formatPercent(resultProb)}</div>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
|
<Row className="items-start justify-between gap-2 text-sm">
|
||||||
<Row>
|
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
||||||
<div>{formatPercent(initialProb)}</div>
|
<div>
|
||||||
<div className="mx-2">→</div>
|
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||||
<div>{formatPercent(resultProb)}</div>
|
</div>
|
||||||
</Row>
|
|
||||||
|
|
||||||
{betChoice && (
|
|
||||||
<>
|
|
||||||
<Spacer h={4} />
|
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
|
||||||
Payout if <OutcomeLabel outcome={betChoice} />
|
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
text={`Current payout for ${formatWithCommas(
|
text={`Current payout for ${formatWithCommas(
|
||||||
shares
|
shares
|
||||||
)} / ${formatWithCommas(
|
)} / ${formatWithCommas(
|
||||||
shares +
|
shares +
|
||||||
totalShares[betChoice] -
|
totalShares[betChoice ?? 'YES'] -
|
||||||
(phantomShares ? phantomShares[betChoice] : 0)
|
(phantomShares ? phantomShares[betChoice ?? 'YES'] : 0)
|
||||||
)} ${betChoice} shares`}
|
)} ${betChoice} shares`}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<div>
|
<Row className="flex-wrap items-end justify-end gap-2">
|
||||||
{formatMoney(currentPayout)}
|
<span className="whitespace-nowrap">
|
||||||
<span>(+{currentReturnPercent})</span>
|
{formatMoney(currentPayout)}
|
||||||
</div>
|
</span>
|
||||||
</>
|
<span>(+{currentReturnPercent})</span>
|
||||||
)}
|
</Row>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={8} />
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn',
|
'btn flex-1',
|
||||||
betDisabled
|
betDisabled
|
||||||
? 'btn-disabled'
|
? 'btn-disabled'
|
||||||
: betChoice === 'YES'
|
: betChoice === 'YES'
|
||||||
|
@ -213,7 +211,7 @@ export function BetPanel(props: {
|
||||||
)}
|
)}
|
||||||
{user === null && (
|
{user === null && (
|
||||||
<button
|
<button
|
||||||
className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||||
onClick={firebaseLogin}
|
onClick={firebaseLogin}
|
||||||
>
|
>
|
||||||
Sign in to trade!
|
Sign in to trade!
|
||||||
|
|
|
@ -82,7 +82,8 @@ export function BetsList(props: { user: User }) {
|
||||||
if (bet.isSold || bet.sale) return 0
|
if (bet.isSold || bet.sale) return 0
|
||||||
|
|
||||||
const contract = contracts.find((c) => c.id === contractId)
|
const contract = contracts.find((c) => c.id === contractId)
|
||||||
return contract ? calculatePayout(contract, bet, 'MKT') : 0
|
const payout = contract ? calculatePayout(contract, bet, 'MKT') : 0
|
||||||
|
return payout - (bet.loanAmount ?? 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -126,8 +127,8 @@ export function BetsList(props: { user: User }) {
|
||||||
const totalPortfolio = currentBetsValue + user.balance
|
const totalPortfolio = currentBetsValue + user.balance
|
||||||
|
|
||||||
const totalPnl = totalPortfolio - user.totalDeposits
|
const totalPnl = totalPortfolio - user.totalDeposits
|
||||||
const totalProfit = (totalPnl / user.totalDeposits) * 100
|
const totalProfitPercent = (totalPnl / user.totalDeposits) * 100
|
||||||
const investedProfit =
|
const investedProfitPercent =
|
||||||
((currentBetsValue - currentInvestment) / currentInvestment) * 100
|
((currentBetsValue - currentInvestment) / currentInvestment) * 100
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -135,17 +136,17 @@ export function BetsList(props: { user: User }) {
|
||||||
<Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0">
|
<Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0">
|
||||||
<Row className="gap-8">
|
<Row className="gap-8">
|
||||||
<Col>
|
<Col>
|
||||||
<div className="text-sm text-gray-500">Invested</div>
|
<div className="text-sm text-gray-500">Investment value</div>
|
||||||
<div className="text-lg">
|
<div className="text-lg">
|
||||||
{formatMoney(currentBetsValue)}{' '}
|
{formatMoney(currentBetsValue)}{' '}
|
||||||
<ProfitBadge profitPercent={investedProfit} />
|
<ProfitBadge profitPercent={investedProfitPercent} />
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<div className="text-sm text-gray-500">Total portfolio</div>
|
<div className="text-sm text-gray-500">Total profit</div>
|
||||||
<div className="text-lg">
|
<div className="text-lg">
|
||||||
{formatMoney(totalPortfolio)}{' '}
|
{formatMoney(totalPnl)}{' '}
|
||||||
<ProfitBadge profitPercent={totalProfit} />
|
<ProfitBadge profitPercent={totalProfitPercent} />
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -457,6 +458,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
shares,
|
shares,
|
||||||
isSold,
|
isSold,
|
||||||
isAnte,
|
isAnte,
|
||||||
|
loanAmount,
|
||||||
} = bet
|
} = bet
|
||||||
|
|
||||||
const { isResolved, closeTime } = contract
|
const { isResolved, closeTime } = contract
|
||||||
|
@ -464,7 +466,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
|
|
||||||
const saleAmount = saleBet?.sale?.amount
|
const saleAmount = saleBet?.sale?.amount
|
||||||
|
|
||||||
const saleDisplay = bet.isAnte ? (
|
const saleDisplay = isAnte ? (
|
||||||
'ANTE'
|
'ANTE'
|
||||||
) : saleAmount !== undefined ? (
|
) : saleAmount !== undefined ? (
|
||||||
<>{formatMoney(saleAmount)} (sold)</>
|
<>{formatMoney(saleAmount)} (sold)</>
|
||||||
|
@ -491,7 +493,10 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
<td>
|
<td>
|
||||||
<OutcomeLabel outcome={outcome} />
|
<OutcomeLabel outcome={outcome} />
|
||||||
</td>
|
</td>
|
||||||
<td>{formatMoney(amount)}</td>
|
<td>
|
||||||
|
{formatMoney(amount)}
|
||||||
|
{loanAmount ? ` (${formatMoney(loanAmount ?? 0)} loan)` : ''}
|
||||||
|
</td>
|
||||||
<td>{saleDisplay}</td>
|
<td>{saleDisplay}</td>
|
||||||
{!isResolved && <td>{payoutIfChosenDisplay}</td>}
|
{!isResolved && <td>{payoutIfChosenDisplay}</td>}
|
||||||
<td>
|
<td>
|
||||||
|
@ -510,21 +515,23 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { contract, bet } = props
|
const { contract, bet } = props
|
||||||
const isBinary = contract.outcomeType === 'BINARY'
|
const { outcome, shares, loanAmount } = bet
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const initialProb = getOutcomeProbability(
|
const initialProb = getOutcomeProbability(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
bet.outcome === 'NO' ? 'YES' : bet.outcome
|
outcome === 'NO' ? 'YES' : outcome
|
||||||
)
|
)
|
||||||
|
|
||||||
const outcomeProb = getProbabilityAfterSale(
|
const outcomeProb = getProbabilityAfterSale(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
bet.outcome,
|
outcome,
|
||||||
bet.shares
|
shares
|
||||||
)
|
)
|
||||||
|
|
||||||
const saleAmount = calculateSaleAmount(contract, bet)
|
const saleAmount = calculateSaleAmount(contract, bet)
|
||||||
|
const profit = saleAmount - bet.amount
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationButton
|
<ConfirmationButton
|
||||||
|
@ -533,7 +540,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
||||||
label: 'Sell',
|
label: 'Sell',
|
||||||
}}
|
}}
|
||||||
submitBtn={{ className: 'btn-primary' }}
|
submitBtn={{ className: 'btn-primary', label: 'Sell' }}
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
await sellBet({ contractId: contract.id, betId: bet.id })
|
await sellBet({ contractId: contract.id, betId: bet.id })
|
||||||
|
@ -541,17 +548,21 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-4 text-2xl">
|
<div className="mb-4 text-2xl">
|
||||||
Sell <OutcomeLabel outcome={bet.outcome} />
|
Sell {formatWithCommas(shares)} shares of{' '}
|
||||||
</div>
|
<OutcomeLabel outcome={outcome} /> for {formatMoney(saleAmount)}?
|
||||||
<div>
|
|
||||||
Do you want to sell {formatWithCommas(bet.shares)} shares of{' '}
|
|
||||||
<OutcomeLabel outcome={bet.outcome} /> for {formatMoney(saleAmount)}?
|
|
||||||
</div>
|
</div>
|
||||||
|
{!!loanAmount && (
|
||||||
|
<div className="mt-2">
|
||||||
|
You will also pay back {formatMoney(loanAmount)} of your loan, for a
|
||||||
|
net of {formatMoney(saleAmount - loanAmount)}.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-2 mb-1 text-sm text-gray-500">
|
<div className="mt-2 mb-1 text-sm">
|
||||||
({isBinary ? 'Updated' : <OutcomeLabel outcome={bet.outcome} />}{' '}
|
{profit > 0 ? 'Profit' : 'Loss'}: {formatMoney(profit).replace('-', '')}
|
||||||
probability: {formatPercent(initialProb)} → {formatPercent(outcomeProb)}
|
<br />
|
||||||
)
|
Market probability: {formatPercent(initialProb)} →{' '}
|
||||||
|
{formatPercent(outcomeProb)}
|
||||||
</div>
|
</div>
|
||||||
</ConfirmationButton>
|
</ConfirmationButton>
|
||||||
)
|
)
|
||||||
|
|
13
web/components/client-render.tsx
Normal file
13
web/components/client-render.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Adapted from https://stackoverflow.com/a/50884055/1222351
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export function ClientRender(props: { children: React.ReactNode }) {
|
||||||
|
const { children } = props
|
||||||
|
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return mounted ? <>{children}</> : null
|
||||||
|
}
|
|
@ -150,7 +150,8 @@ function AbbrContractDetails(props: {
|
||||||
) : showCloseTime ? (
|
) : showCloseTime ? (
|
||||||
<Row className="gap-1">
|
<Row className="gap-1">
|
||||||
<ClockIcon className="h-5 w-5" />
|
<ClockIcon className="h-5 w-5" />
|
||||||
Closes {fromNow(closeTime || 0)}
|
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
|
||||||
|
{fromNow(closeTime || 0)}
|
||||||
</Row>
|
</Row>
|
||||||
) : (
|
) : (
|
||||||
<Row className="gap-1">
|
<Row className="gap-1">
|
||||||
|
@ -312,7 +313,7 @@ function EditableCloseDate(props: {
|
||||||
className="btn btn-xs btn-ghost"
|
className="btn btn-xs btn-ghost"
|
||||||
onClick={() => setIsEditingCloseTime(true)}
|
onClick={() => setIsEditingCloseTime(true)}
|
||||||
>
|
>
|
||||||
<PencilIcon className="inline h-4 w-4 mr-2" /> Edit
|
<PencilIcon className="mr-2 inline h-4 w-4" /> Edit
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -25,7 +25,7 @@ import {
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { Linkify } from './linkify'
|
import { Linkify } from './linkify'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { createComment } from '../lib/firebase/comments'
|
import { createComment, MAX_COMMENT_LENGTH } from '../lib/firebase/comments'
|
||||||
import { useComments } from '../hooks/use-comments'
|
import { useComments } from '../hooks/use-comments'
|
||||||
import { formatMoney } from '../../common/util/format'
|
import { formatMoney } from '../../common/util/format'
|
||||||
import { ResolutionOrChance } from './contract-card'
|
import { ResolutionOrChance } from './contract-card'
|
||||||
|
@ -135,8 +135,9 @@ function FeedBet(props: { activityItem: any; feedType: FeedType }) {
|
||||||
className="textarea textarea-bordered w-full"
|
className="textarea textarea-bordered w-full"
|
||||||
placeholder="Add a comment..."
|
placeholder="Add a comment..."
|
||||||
rows={3}
|
rows={3}
|
||||||
|
maxLength={MAX_COMMENT_LENGTH}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && e.ctrlKey) {
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||||
submitComment()
|
submitComment()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -181,7 +182,7 @@ function EditContract(props: {
|
||||||
e.target.setSelectionRange(text.length, text.length)
|
e.target.setSelectionRange(text.length, text.length)
|
||||||
}
|
}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && e.ctrlKey) {
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||||
onSave(text)
|
onSave(text)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -289,7 +290,10 @@ function TruncatedComment(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 whitespace-pre-line break-words text-gray-700">
|
<div
|
||||||
|
className="mt-2 whitespace-pre-line break-words text-gray-700"
|
||||||
|
style={{ fontSize: 15 }}
|
||||||
|
>
|
||||||
<Linkify text={truncated} />
|
<Linkify text={truncated} />
|
||||||
{truncated != comment && (
|
{truncated != comment && (
|
||||||
<SiteLink href={moreHref} className="text-indigo-700">
|
<SiteLink href={moreHref} className="text-indigo-700">
|
||||||
|
@ -300,8 +304,11 @@ function TruncatedComment(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeedQuestion(props: { contract: Contract }) {
|
function FeedQuestion(props: {
|
||||||
const { contract } = props
|
contract: Contract
|
||||||
|
showDescription?: boolean
|
||||||
|
}) {
|
||||||
|
const { contract, showDescription } = props
|
||||||
const { creatorName, creatorUsername, question, resolution, outcomeType } =
|
const { creatorName, creatorUsername, question, resolution, outcomeType } =
|
||||||
contract
|
contract
|
||||||
const { truePool } = contractMetrics(contract)
|
const { truePool } = contractMetrics(contract)
|
||||||
|
@ -336,22 +343,34 @@ function FeedQuestion(props: { contract: Contract }) {
|
||||||
{closeMessage}
|
{closeMessage}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Col className="mb-4 items-start justify-between gap-2 sm:flex-row sm:gap-4">
|
<Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4">
|
||||||
<SiteLink
|
<Col>
|
||||||
href={contractPath(contract)}
|
<SiteLink
|
||||||
className="text-lg text-indigo-700 sm:text-xl"
|
href={contractPath(contract)}
|
||||||
>
|
className="text-lg text-indigo-700 sm:text-xl"
|
||||||
{question}
|
>
|
||||||
</SiteLink>
|
{question}
|
||||||
|
</SiteLink>
|
||||||
|
{!showDescription && (
|
||||||
|
<SiteLink
|
||||||
|
href={contractPath(contract)}
|
||||||
|
className="relative top-4 self-end text-sm sm:self-start"
|
||||||
|
>
|
||||||
|
<div className="pb-1.5 text-gray-500">See more...</div>
|
||||||
|
</SiteLink>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
{(isBinary || resolution) && (
|
{(isBinary || resolution) && (
|
||||||
<ResolutionOrChance className="items-center" contract={contract} />
|
<ResolutionOrChance className="items-center" contract={contract} />
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<TruncatedComment
|
{showDescription && (
|
||||||
comment={contract.description}
|
<TruncatedComment
|
||||||
moreHref={contractPath(contract)}
|
comment={contract.description}
|
||||||
shouldTruncate
|
moreHref={contractPath(contract)}
|
||||||
/>
|
shouldTruncate
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -680,6 +699,7 @@ type ActivityItem = {
|
||||||
| 'close'
|
| 'close'
|
||||||
| 'resolve'
|
| 'resolve'
|
||||||
| 'expand'
|
| 'expand'
|
||||||
|
| undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeedType =
|
type FeedType =
|
||||||
|
@ -690,64 +710,24 @@ type FeedType =
|
||||||
// Grouped for a multi-category outcome
|
// Grouped for a multi-category outcome
|
||||||
| 'multi'
|
| 'multi'
|
||||||
|
|
||||||
export function ContractFeed(props: {
|
function FeedItems(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
items: ActivityItem[]
|
||||||
comments: Comment[]
|
|
||||||
feedType: FeedType
|
feedType: FeedType
|
||||||
|
setExpanded: (expanded: boolean) => void
|
||||||
outcome?: string // Which multi-category outcome to filter
|
outcome?: string // Which multi-category outcome to filter
|
||||||
betRowClassName?: string
|
betRowClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, feedType, outcome, betRowClassName } = props
|
const { contract, items, feedType, outcome, setExpanded, betRowClassName } =
|
||||||
const { id, outcomeType } = contract
|
props
|
||||||
|
const { outcomeType } = contract
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
|
||||||
const user = useUser()
|
|
||||||
|
|
||||||
let bets = useBets(contract.id) ?? props.bets
|
|
||||||
bets = isBinary
|
|
||||||
? bets.filter((bet) => !bet.isAnte)
|
|
||||||
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
|
||||||
|
|
||||||
if (feedType === 'multi') {
|
|
||||||
bets = bets.filter((bet) => bet.outcome === outcome)
|
|
||||||
}
|
|
||||||
|
|
||||||
const comments = useComments(id) ?? props.comments
|
|
||||||
|
|
||||||
const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
|
|
||||||
|
|
||||||
const allItems = [
|
|
||||||
{ type: 'start', id: 0 },
|
|
||||||
...groupBets(bets, comments, groupWindow, user?.id),
|
|
||||||
]
|
|
||||||
if (contract.closeTime && contract.closeTime <= Date.now()) {
|
|
||||||
allItems.push({ type: 'close', id: `${contract.closeTime}` })
|
|
||||||
}
|
|
||||||
if (contract.resolution) {
|
|
||||||
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
|
||||||
}
|
|
||||||
if (feedType === 'multi') {
|
|
||||||
// Hack to add some more padding above the 'multi' feedType, by adding a null item
|
|
||||||
allItems.unshift({ type: '', id: -1 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are more than 5 items, only show the first, an expand item, and last 3
|
|
||||||
let items = allItems
|
|
||||||
if (!expanded && allItems.length > 5 && feedType == 'activity') {
|
|
||||||
items = [
|
|
||||||
allItems[0],
|
|
||||||
{ type: 'expand', id: 'expand' },
|
|
||||||
...allItems.slice(-3),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flow-root pr-2 md:pr-0">
|
<div className="flow-root pr-2 md:pr-0">
|
||||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
|
||||||
{items.map((activityItem, activityItemIdx) => (
|
{items.map((activityItem, activityItemIdx) => (
|
||||||
<div key={activityItem.id} className="relative pb-8">
|
<div key={activityItem.id} className="relative pb-6">
|
||||||
{activityItemIdx !== items.length - 1 ? (
|
{activityItemIdx !== items.length - 1 ? (
|
||||||
<span
|
<span
|
||||||
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
||||||
|
@ -791,6 +771,117 @@ export function ContractFeed(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ContractFeed(props: {
|
||||||
|
contract: Contract
|
||||||
|
bets: Bet[]
|
||||||
|
comments: Comment[]
|
||||||
|
feedType: FeedType
|
||||||
|
outcome?: string // Which multi-category outcome to filter
|
||||||
|
betRowClassName?: string
|
||||||
|
}) {
|
||||||
|
const { contract, feedType, outcome, betRowClassName } = props
|
||||||
|
const { id, outcomeType } = contract
|
||||||
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
let bets = useBets(contract.id) ?? props.bets
|
||||||
|
bets = isBinary
|
||||||
|
? bets.filter((bet) => !bet.isAnte)
|
||||||
|
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
||||||
|
|
||||||
|
if (feedType === 'multi') {
|
||||||
|
bets = bets.filter((bet) => bet.outcome === outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments = useComments(id) ?? props.comments
|
||||||
|
|
||||||
|
const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
|
||||||
|
|
||||||
|
const allItems: ActivityItem[] = [
|
||||||
|
{ type: 'start', id: '0' },
|
||||||
|
...groupBets(bets, comments, groupWindow, user?.id),
|
||||||
|
]
|
||||||
|
if (contract.closeTime && contract.closeTime <= Date.now()) {
|
||||||
|
allItems.push({ type: 'close', id: `${contract.closeTime}` })
|
||||||
|
}
|
||||||
|
if (contract.resolution) {
|
||||||
|
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
||||||
|
}
|
||||||
|
if (feedType === 'multi') {
|
||||||
|
// Hack to add some more padding above the 'multi' feedType, by adding a null item
|
||||||
|
allItems.unshift({ type: undefined, id: '-1' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are more than 5 items, only show the first, an expand item, and last 3
|
||||||
|
let items = allItems
|
||||||
|
if (!expanded && allItems.length > 5 && feedType == 'activity') {
|
||||||
|
items = [
|
||||||
|
allItems[0],
|
||||||
|
{ type: 'expand', id: 'expand' },
|
||||||
|
...allItems.slice(-3),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeedItems
|
||||||
|
contract={contract}
|
||||||
|
items={items}
|
||||||
|
feedType={feedType}
|
||||||
|
setExpanded={setExpanded}
|
||||||
|
betRowClassName={betRowClassName}
|
||||||
|
outcome={outcome}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContractActivityFeed(props: {
|
||||||
|
contract: Contract
|
||||||
|
bets: Bet[]
|
||||||
|
comments: Comment[]
|
||||||
|
betRowClassName?: string
|
||||||
|
}) {
|
||||||
|
const { contract, betRowClassName, bets, comments } = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
bets.sort((b1, b2) => b1.createdTime - b2.createdTime)
|
||||||
|
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
|
||||||
|
const allItems: ActivityItem[] = [
|
||||||
|
{ type: 'start', id: '0' },
|
||||||
|
...groupBets(bets, comments, DAY_IN_MS, user?.id),
|
||||||
|
]
|
||||||
|
if (contract.closeTime && contract.closeTime <= Date.now()) {
|
||||||
|
allItems.push({ type: 'close', id: `${contract.closeTime}` })
|
||||||
|
}
|
||||||
|
if (contract.resolution) {
|
||||||
|
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all but last bet group.
|
||||||
|
const betGroups = allItems.filter((item) => item.type === 'betgroup')
|
||||||
|
const lastBetGroup = betGroups[betGroups.length - 1]
|
||||||
|
const filtered = allItems.filter(
|
||||||
|
(item) => item.type !== 'betgroup' || item.id === lastBetGroup?.id
|
||||||
|
)
|
||||||
|
|
||||||
|
// Only show the first item plus the last three items.
|
||||||
|
const items =
|
||||||
|
filtered.length > 3 ? [filtered[0], ...filtered.slice(-3)] : filtered
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeedItems
|
||||||
|
contract={contract}
|
||||||
|
items={items}
|
||||||
|
feedType="activity"
|
||||||
|
setExpanded={() => {}}
|
||||||
|
betRowClassName={betRowClassName}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function ContractSummaryFeed(props: {
|
export function ContractSummaryFeed(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
betRowClassName?: string
|
betRowClassName?: string
|
||||||
|
@ -804,7 +895,7 @@ export function ContractSummaryFeed(props: {
|
||||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
||||||
<div className="relative pb-8">
|
<div className="relative pb-8">
|
||||||
<div className="relative flex items-start space-x-3">
|
<div className="relative flex items-start space-x-3">
|
||||||
<FeedQuestion contract={contract} />
|
<FeedQuestion contract={contract} showDescription />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -83,7 +83,7 @@ export function ContractProbGraph(props: { contract: Contract; bets: Bet[] }) {
|
||||||
format: (time) => formatTime(+time, lessThanAWeek),
|
format: (time) => formatTime(+time, lessThanAWeek),
|
||||||
}}
|
}}
|
||||||
colors={{ datum: 'color' }}
|
colors={{ datum: 'color' }}
|
||||||
pointSize={10}
|
pointSize={bets.length > 100 ? 0 : 10}
|
||||||
pointBorderWidth={1}
|
pointBorderWidth={1}
|
||||||
pointBorderColor="#fff"
|
pointBorderColor="#fff"
|
||||||
enableSlices="x"
|
enableSlices="x"
|
||||||
|
|
|
@ -229,11 +229,13 @@ export function SearchableGrid(props: {
|
||||||
)
|
)
|
||||||
} else if (sort === 'oldest') {
|
} else if (sort === 'oldest') {
|
||||||
matches.sort((a, b) => a.createdTime - b.createdTime)
|
matches.sort((a, b) => a.createdTime - b.createdTime)
|
||||||
} else if (sort === 'close-date') {
|
} else if (sort === 'close-date' || sort === 'closed') {
|
||||||
matches = _.sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
|
matches = _.sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
|
||||||
matches = _.sortBy(matches, (contract) => contract.closeTime)
|
matches = _.sortBy(matches, (contract) => contract.closeTime)
|
||||||
// Hide contracts that have already closed
|
const hideClosed = sort === 'closed'
|
||||||
matches = matches.filter(({ closeTime }) => (closeTime || 0) > Date.now())
|
matches = matches.filter(
|
||||||
|
({ closeTime }) => closeTime && closeTime > Date.now() !== hideClosed
|
||||||
|
)
|
||||||
} else if (sort === 'most-traded') {
|
} else if (sort === 'most-traded') {
|
||||||
matches.sort(
|
matches.sort(
|
||||||
(a, b) => contractMetrics(b).truePool - contractMetrics(a).truePool
|
(a, b) => contractMetrics(b).truePool - contractMetrics(a).truePool
|
||||||
|
@ -272,6 +274,7 @@ export function SearchableGrid(props: {
|
||||||
<option value="most-traded">Most traded</option>
|
<option value="most-traded">Most traded</option>
|
||||||
<option value="24-hour-vol">24h volume</option>
|
<option value="24-hour-vol">24h volume</option>
|
||||||
<option value="close-date">Closing soon</option>
|
<option value="close-date">Closing soon</option>
|
||||||
|
<option value="closed">Closed</option>
|
||||||
<option value="newest">Newest</option>
|
<option value="newest">Newest</option>
|
||||||
<option value="oldest">Oldest</option>
|
<option value="oldest">Oldest</option>
|
||||||
|
|
||||||
|
@ -289,7 +292,7 @@ export function SearchableGrid(props: {
|
||||||
) : (
|
) : (
|
||||||
<ContractsGrid
|
<ContractsGrid
|
||||||
contracts={matches}
|
contracts={matches}
|
||||||
showCloseTime={sort == 'close-date'}
|
showCloseTime={['close-date', 'closed'].includes(sort)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import dayjs from 'dayjs'
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
import advanced from 'dayjs/plugin/advancedFormat'
|
import advanced from 'dayjs/plugin/advancedFormat'
|
||||||
|
import { ClientRender } from './client-render'
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
@ -19,13 +20,15 @@ export function DateTimeTooltip(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span
|
<ClientRender>
|
||||||
className="tooltip hidden cursor-default sm:inline-block"
|
<span
|
||||||
data-tip={toolTip}
|
className="tooltip hidden cursor-default sm:inline-block"
|
||||||
>
|
data-tip={toolTip}
|
||||||
{props.children}
|
>
|
||||||
</span>
|
{props.children}
|
||||||
<span className="sm:hidden whitespace-nowrap">{props.children}</span>
|
</span>
|
||||||
|
</ClientRender>
|
||||||
|
<span className="whitespace-nowrap sm:hidden">{props.children}</span>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ export const FastFoldFollowing = (props: {
|
||||||
user={user}
|
user={user}
|
||||||
followedFoldSlugs={followedFoldSlugs}
|
followedFoldSlugs={followedFoldSlugs}
|
||||||
folds={[
|
folds={[
|
||||||
{ name: 'Politics', slug: 'politics' },
|
{ name: 'Russia/Ukraine', slug: 'russia-ukraine' },
|
||||||
{ name: 'Crypto', slug: 'crypto' },
|
{ name: 'Crypto', slug: 'crypto' },
|
||||||
{ name: 'Sports', slug: 'sports' },
|
{ name: 'Sports', slug: 'sports' },
|
||||||
{ name: 'Science', slug: 'science' },
|
{ name: 'Science', slug: 'science' },
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { SparklesIcon, XIcon } from '@heroicons/react/solid'
|
||||||
import { Avatar } from './avatar'
|
import { Avatar } from './avatar'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { NewContract } from '../pages/create'
|
import { NewContract } from '../pages/create'
|
||||||
import { firebaseLogin, User } from '../lib/firebase/users'
|
import { firebaseLogin, User } from '../lib/firebase/users'
|
||||||
|
@ -7,44 +8,51 @@ import { ContractsGrid } from './contracts-list'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
|
||||||
export function FeedPromo(props: { hotContracts: Contract[] }) {
|
export function FeedPromo(props: { hotContracts: Contract[] }) {
|
||||||
const { hotContracts } = props
|
const { hotContracts } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Col className="w-full bg-white p-6 sm:rounded-lg">
|
<Col className="m-6 mb-1 text-center sm:m-12">
|
||||||
<h1 className="mt-4 text-4xl sm:mt-5 sm:text-6xl lg:mt-6 xl:text-6xl">
|
<h1 className="mt-4 text-4xl sm:mt-5 sm:text-6xl lg:mt-6 xl:text-6xl">
|
||||||
<div className="mb-2">Create your own</div>
|
<div className="font-semibold sm:mb-2">
|
||||||
<div className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent">
|
A{' '}
|
||||||
prediction markets
|
<span className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent">
|
||||||
|
market{' '}
|
||||||
|
</span>
|
||||||
|
for
|
||||||
|
</div>
|
||||||
|
<div className="font-semibold">
|
||||||
|
every{' '}
|
||||||
|
<span className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent">
|
||||||
|
prediction
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
<div className="mb-4 text-gray-500">
|
<div className="mb-4 text-gray-500">
|
||||||
Find prediction markets run by your favorite creators, or make your
|
Find prediction markets on any topic imaginable. Or create your own!
|
||||||
own.
|
|
||||||
<br />
|
<br />
|
||||||
Sign up to get M$ 1,000 for free and start trading!
|
Sign up to get M$ 1,000 and start trading.
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
<button
|
<button
|
||||||
className="btn btn-lg self-center border-none bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
|
className="btn btn-lg self-center border-none bg-gradient-to-r from-teal-500 to-green-500 normal-case hover:from-teal-600 hover:to-green-600"
|
||||||
onClick={firebaseLogin}
|
onClick={firebaseLogin}
|
||||||
>
|
>
|
||||||
Sign up now
|
Sign up for free
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={12} />
|
||||||
{/*
|
|
||||||
<TagsList
|
|
||||||
className="mt-2"
|
|
||||||
tags={['#politics', '#crypto', '#covid', '#sports', '#meta']}
|
|
||||||
/>
|
|
||||||
<Spacer h={6} /> */}
|
|
||||||
|
|
||||||
|
<Row className="m-4 mb-6 items-center gap-1 text-xl font-semibold text-gray-800">
|
||||||
|
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
|
||||||
|
Trending today
|
||||||
|
</Row>
|
||||||
<ContractsGrid
|
<ContractsGrid
|
||||||
contracts={hotContracts?.slice(0, 10) || []}
|
contracts={hotContracts?.slice(0, 10) || []}
|
||||||
showHotVolume
|
showHotVolume
|
||||||
|
@ -61,7 +69,8 @@ export default function FeedCreate(props: {
|
||||||
}) {
|
}) {
|
||||||
const { user, tag, className } = props
|
const { user, tag, className } = props
|
||||||
const [question, setQuestion] = useState('')
|
const [question, setQuestion] = useState('')
|
||||||
const [focused, setFocused] = useState(false)
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement | null>()
|
||||||
|
|
||||||
const placeholders = [
|
const placeholders = [
|
||||||
'Will anyone I know get engaged this year?',
|
'Will anyone I know get engaged this year?',
|
||||||
|
@ -78,60 +87,60 @@ export default function FeedCreate(props: {
|
||||||
)
|
)
|
||||||
const placeholder = props.placeholder ?? `e.g. ${placeholders[randIndex]}`
|
const placeholder = props.placeholder ?? `e.g. ${placeholders[randIndex]}`
|
||||||
|
|
||||||
const panelRef = useRef<HTMLElement | null>()
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement | null>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onClick = () => {
|
|
||||||
if (
|
|
||||||
panelRef.current &&
|
|
||||||
document.activeElement &&
|
|
||||||
!panelRef.current.contains(document.activeElement)
|
|
||||||
)
|
|
||||||
setFocused(false)
|
|
||||||
}
|
|
||||||
window.addEventListener('click', onClick)
|
|
||||||
return () => window.removeEventListener('click', onClick)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mt-2 w-full rounded bg-white p-4 shadow-md',
|
'mt-2 w-full cursor-text rounded bg-white p-4 shadow-md',
|
||||||
question || focused ? 'ring-2 ring-indigo-300' : '',
|
isExpanded ? 'ring-2 ring-indigo-300' : '',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={() => !focused && inputRef.current?.focus()}
|
onClick={() => {
|
||||||
ref={(elem) => (panelRef.current = elem)}
|
!isExpanded && inputRef.current?.focus()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative flex items-start space-x-3">
|
<div className="relative flex items-start space-x-3">
|
||||||
<Avatar username={user?.username} avatarUrl={user?.avatarUrl} noLink />
|
<Avatar username={user?.username} avatarUrl={user?.avatarUrl} noLink />
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{/* TODO: Show focus, for accessibility */}
|
<Row className="justify-between">
|
||||||
<div>
|
|
||||||
<p className="my-0.5 text-sm">Ask a question... </p>
|
<p className="my-0.5 text-sm">Ask a question... </p>
|
||||||
</div>
|
{isExpanded && (
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-circle btn-ghost rounded"
|
||||||
|
onClick={() => setIsExpanded(false)}
|
||||||
|
>
|
||||||
|
<XIcon
|
||||||
|
className="mx-auto h-6 w-6 text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef as any}
|
ref={inputRef as any}
|
||||||
className="w-full resize-none appearance-none border-transparent bg-transparent p-0 text-lg text-indigo-700 placeholder:text-gray-400 focus:border-transparent focus:ring-transparent sm:text-xl"
|
className={clsx(
|
||||||
|
'w-full resize-none appearance-none border-transparent bg-transparent p-0 text-indigo-700 placeholder:text-gray-400 focus:border-transparent focus:ring-transparent',
|
||||||
|
question && 'text-lg sm:text-xl',
|
||||||
|
!question && 'text-base sm:text-lg'
|
||||||
|
)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={question}
|
value={question}
|
||||||
|
rows={question.length > 68 ? 4 : 2}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setQuestion(e.target.value.replace('\n', ''))}
|
onChange={(e) => setQuestion(e.target.value.replace('\n', ''))}
|
||||||
onFocus={() => setFocused(true)}
|
onFocus={() => setIsExpanded(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hide component instead of deleting, so edits to NewContract don't get lost */}
|
{/* Hide component instead of deleting, so edits to NewContract don't get lost */}
|
||||||
<div className={question || focused ? '' : 'hidden'}>
|
<div className={isExpanded ? '' : 'hidden'}>
|
||||||
<NewContract question={question} tag={tag} />
|
<NewContract question={question} tag={tag} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
|
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
|
||||||
{!(question || focused) && (
|
{!isExpanded && (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end sm:-mt-4">
|
||||||
<button className="btn btn-sm" disabled>
|
<button className="btn btn-sm" disabled>
|
||||||
Create Market
|
Create Market
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -4,11 +4,11 @@ import { useMemo, useRef } from 'react'
|
||||||
import { Fold } from '../../common/fold'
|
import { Fold } from '../../common/fold'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { filterDefined } from '../../common/util/array'
|
import { filterDefined } from '../../common/util/array'
|
||||||
import { Bet, getRecentBets } from '../lib/firebase/bets'
|
import { Bet } from '../lib/firebase/bets'
|
||||||
import { Comment, getRecentComments } from '../lib/firebase/comments'
|
import { Comment, getRecentComments } from '../lib/firebase/comments'
|
||||||
import { Contract, getActiveContracts } from '../lib/firebase/contracts'
|
import { Contract, getActiveContracts } from '../lib/firebase/contracts'
|
||||||
import { listAllFolds } from '../lib/firebase/folds'
|
import { listAllFolds } from '../lib/firebase/folds'
|
||||||
import { findActiveContracts } from '../pages/activity'
|
import { findActiveContracts } from '../components/activity-feed'
|
||||||
import { useInactiveContracts } from './use-contracts'
|
import { useInactiveContracts } from './use-contracts'
|
||||||
import { useFollowedFolds } from './use-fold'
|
import { useFollowedFolds } from './use-fold'
|
||||||
import { useUserBetContracts } from './use-user-bets'
|
import { useUserBetContracts } from './use-user-bets'
|
||||||
|
@ -20,12 +20,9 @@ export const getAllContractInfo = async () => {
|
||||||
listAllFolds().catch(() => []),
|
listAllFolds().catch(() => []),
|
||||||
])
|
])
|
||||||
|
|
||||||
const [recentBets, recentComments] = await Promise.all([
|
const recentComments = await getRecentComments()
|
||||||
getRecentBets(),
|
|
||||||
getRecentComments(),
|
|
||||||
])
|
|
||||||
|
|
||||||
return { contracts, recentBets, recentComments, folds }
|
return { contracts, recentComments, folds }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFilterYourContracts = (
|
export const useFilterYourContracts = (
|
||||||
|
|
|
@ -10,6 +10,7 @@ export type Sort =
|
||||||
| 'most-traded'
|
| 'most-traded'
|
||||||
| '24-hour-vol'
|
| '24-hour-vol'
|
||||||
| 'close-date'
|
| 'close-date'
|
||||||
|
| 'closed'
|
||||||
| 'resolved'
|
| 'resolved'
|
||||||
| 'all'
|
| 'all'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Bet, listenForUserBets } from '../lib/firebase/bets'
|
import {
|
||||||
|
Bet,
|
||||||
|
listenForUserBets,
|
||||||
|
listenForUserContractBets,
|
||||||
|
} from '../lib/firebase/bets'
|
||||||
|
|
||||||
export const useUserBets = (userId: string | undefined) => {
|
export const useUserBets = (userId: string | undefined) => {
|
||||||
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
||||||
|
@ -12,6 +16,20 @@ export const useUserBets = (userId: string | undefined) => {
|
||||||
return bets
|
return bets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useUserContractBets = (
|
||||||
|
userId: string | undefined,
|
||||||
|
contractId: string | undefined
|
||||||
|
) => {
|
||||||
|
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId && contractId)
|
||||||
|
return listenForUserContractBets(userId, contractId, setBets)
|
||||||
|
}, [userId, contractId])
|
||||||
|
|
||||||
|
return bets
|
||||||
|
}
|
||||||
|
|
||||||
export const useUserBetContracts = (userId: string | undefined) => {
|
export const useUserBetContracts = (userId: string | undefined) => {
|
||||||
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,21 @@ export function listenForUserBets(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listenForUserContractBets(
|
||||||
|
userId: string,
|
||||||
|
contractId: string,
|
||||||
|
setBets: (bets: Bet[]) => void
|
||||||
|
) {
|
||||||
|
const betsQuery = query(
|
||||||
|
collection(db, 'contracts', contractId, 'bets'),
|
||||||
|
where('userId', '==', userId)
|
||||||
|
)
|
||||||
|
return listenForValues<Bet>(betsQuery, (bets) => {
|
||||||
|
bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime)
|
||||||
|
setBets(bets)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function withoutAnteBets(contract: Contract, bets?: Bet[]) {
|
export function withoutAnteBets(contract: Contract, bets?: Bet[]) {
|
||||||
const { createdTime } = contract
|
const { createdTime } = contract
|
||||||
|
|
||||||
|
@ -88,3 +103,24 @@ export function withoutAnteBets(contract: Contract, bets?: Bet[]) {
|
||||||
|
|
||||||
return bets?.filter((bet) => !bet.isAnte) ?? []
|
return bets?.filter((bet) => !bet.isAnte) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getBetsQuery = (startTime: number, endTime: number) =>
|
||||||
|
query(
|
||||||
|
collectionGroup(db, 'bets'),
|
||||||
|
where('createdTime', '>=', startTime),
|
||||||
|
where('createdTime', '<', endTime),
|
||||||
|
orderBy('createdTime', 'asc')
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function getDailyBets(startTime: number, numberOfDays: number) {
|
||||||
|
const query = getBetsQuery(startTime, startTime + DAY_IN_MS * numberOfDays)
|
||||||
|
const bets = await getValues<Bet>(query)
|
||||||
|
|
||||||
|
const betsByDay = _.range(0, numberOfDays).map(() => [] as Bet[])
|
||||||
|
for (const bet of bets) {
|
||||||
|
const dayIndex = Math.floor((bet.createdTime - startTime) / DAY_IN_MS)
|
||||||
|
betsByDay[dayIndex].push(bet)
|
||||||
|
}
|
||||||
|
|
||||||
|
return betsByDay
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import { getValues, listenForValues } from './utils'
|
import { getValues, listenForValues } from './utils'
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
|
@ -14,6 +15,8 @@ import { User } from '../../../common/user'
|
||||||
import { Comment } from '../../../common/comment'
|
import { Comment } from '../../../common/comment'
|
||||||
export type { Comment }
|
export type { Comment }
|
||||||
|
|
||||||
|
export const MAX_COMMENT_LENGTH = 10000
|
||||||
|
|
||||||
export async function createComment(
|
export async function createComment(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
betId: string,
|
betId: string,
|
||||||
|
@ -27,7 +30,7 @@ export async function createComment(
|
||||||
contractId,
|
contractId,
|
||||||
betId,
|
betId,
|
||||||
userId: commenter.id,
|
userId: commenter.id,
|
||||||
text,
|
text: text.slice(0, MAX_COMMENT_LENGTH),
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
userName: commenter.name,
|
userName: commenter.name,
|
||||||
userUsername: commenter.username,
|
userUsername: commenter.username,
|
||||||
|
@ -87,3 +90,30 @@ export function listenForRecentComments(
|
||||||
) {
|
) {
|
||||||
return listenForValues<Comment>(recentCommentsQuery, setComments)
|
return listenForValues<Comment>(recentCommentsQuery, setComments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCommentsQuery = (startTime: number, endTime: number) =>
|
||||||
|
query(
|
||||||
|
collectionGroup(db, 'comments'),
|
||||||
|
where('createdTime', '>=', startTime),
|
||||||
|
where('createdTime', '<', endTime),
|
||||||
|
orderBy('createdTime', 'asc')
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function getDailyComments(
|
||||||
|
startTime: number,
|
||||||
|
numberOfDays: number
|
||||||
|
) {
|
||||||
|
const query = getCommentsQuery(
|
||||||
|
startTime,
|
||||||
|
startTime + DAY_IN_MS * numberOfDays
|
||||||
|
)
|
||||||
|
const comments = await getValues<Comment>(query)
|
||||||
|
|
||||||
|
const commentsByDay = _.range(0, numberOfDays).map(() => [] as Comment[])
|
||||||
|
for (const comment of comments) {
|
||||||
|
const dayIndex = Math.floor((comment.createdTime - startTime) / DAY_IN_MS)
|
||||||
|
commentsByDay[dayIndex].push(comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commentsByDay
|
||||||
|
}
|
||||||
|
|
|
@ -209,3 +209,32 @@ export async function getClosingSoonContracts() {
|
||||||
(contract) => contract.closeTime
|
(contract) => contract.closeTime
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getContractsQuery = (startTime: number, endTime: number) =>
|
||||||
|
query(
|
||||||
|
collection(db, 'contracts'),
|
||||||
|
where('createdTime', '>=', startTime),
|
||||||
|
where('createdTime', '<', endTime),
|
||||||
|
orderBy('createdTime', 'asc')
|
||||||
|
)
|
||||||
|
|
||||||
|
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
export async function getDailyContracts(
|
||||||
|
startTime: number,
|
||||||
|
numberOfDays: number
|
||||||
|
) {
|
||||||
|
const query = getContractsQuery(
|
||||||
|
startTime,
|
||||||
|
startTime + DAY_IN_MS * numberOfDays
|
||||||
|
)
|
||||||
|
const contracts = await getValues<Contract>(query)
|
||||||
|
|
||||||
|
const contractsByDay = _.range(0, numberOfDays).map(() => [] as Contract[])
|
||||||
|
for (const contract of contracts) {
|
||||||
|
const dayIndex = Math.floor((contract.createdTime - startTime) / DAY_IN_MS)
|
||||||
|
contractsByDay[dayIndex].push(contract)
|
||||||
|
}
|
||||||
|
|
||||||
|
return contractsByDay
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import {
|
import {
|
||||||
doc,
|
|
||||||
getDoc,
|
getDoc,
|
||||||
getDocs,
|
getDocs,
|
||||||
onSnapshot,
|
onSnapshot,
|
||||||
|
@ -22,7 +21,9 @@ export function listenForValue<T>(
|
||||||
docRef: DocumentReference,
|
docRef: DocumentReference,
|
||||||
setValue: (value: T | null) => void
|
setValue: (value: T | null) => void
|
||||||
) {
|
) {
|
||||||
return onSnapshot(docRef, (snapshot) => {
|
// Exclude cached snapshots so we only trigger on fresh data.
|
||||||
|
// includeMetadataChanges ensures listener is called even when server data is the same as cached data.
|
||||||
|
return onSnapshot(docRef, { includeMetadataChanges: true }, (snapshot) => {
|
||||||
if (snapshot.metadata.fromCache) return
|
if (snapshot.metadata.fromCache) return
|
||||||
|
|
||||||
const value = snapshot.exists() ? (snapshot.data() as T) : null
|
const value = snapshot.exists() ? (snapshot.data() as T) : null
|
||||||
|
@ -34,7 +35,9 @@ export function listenForValues<T>(
|
||||||
query: Query,
|
query: Query,
|
||||||
setValues: (values: T[]) => void
|
setValues: (values: T[]) => void
|
||||||
) {
|
) {
|
||||||
return onSnapshot(query, (snapshot) => {
|
// Exclude cached snapshots so we only trigger on fresh data.
|
||||||
|
// includeMetadataChanges ensures listener is called even when server data is the same as cached data.
|
||||||
|
return onSnapshot(query, { includeMetadataChanges: true }, (snapshot) => {
|
||||||
if (snapshot.metadata.fromCache) return
|
if (snapshot.metadata.fromCache) return
|
||||||
|
|
||||||
const values = snapshot.docs.map((doc) => doc.data() as T)
|
const values = snapshot.docs.map((doc) => doc.data() as T)
|
||||||
|
|
|
@ -127,7 +127,7 @@ export default function ContractPage(props: {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Col className="w-full justify-between md:flex-row">
|
<Col className="w-full justify-between md:flex-row">
|
||||||
<div className="flex-[3] rounded border-0 border-gray-100 bg-white px-2 py-6 md:px-6 md:py-8">
|
<div className="flex-1 rounded border-0 border-gray-100 bg-white px-2 py-6 md:px-6 md:py-8">
|
||||||
<ContractOverview
|
<ContractOverview
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets ?? []}
|
bets={bets ?? []}
|
||||||
|
@ -154,9 +154,9 @@ export default function ContractPage(props: {
|
||||||
<>
|
<>
|
||||||
<div className="md:ml-6" />
|
<div className="md:ml-6" />
|
||||||
|
|
||||||
<Col className="flex-1">
|
<Col className="flex-shrink-0 md:w-[310px]">
|
||||||
{allowTrade && (
|
{allowTrade && (
|
||||||
<BetPanel className="hidden lg:inline" contract={contract} />
|
<BetPanel className="hidden lg:flex" contract={contract} />
|
||||||
)}
|
)}
|
||||||
{allowResolve && (
|
{allowResolve && (
|
||||||
<ResolutionPanel creator={user} contract={contract} />
|
<ResolutionPanel creator={user} contract={contract} />
|
||||||
|
|
|
@ -118,6 +118,13 @@ function Contents() {
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
<p>
|
||||||
|
More questions? Check out{' '}
|
||||||
|
<a href="https://outsidetheasylum.blog/manifold-markets-faq/">
|
||||||
|
this community-driven FAQ
|
||||||
|
</a>
|
||||||
|
!
|
||||||
|
</p>
|
||||||
<h3 id="can-prediction-markets-work-without-real-money-">
|
<h3 id="can-prediction-markets-work-without-real-money-">
|
||||||
Can prediction markets work without real money?
|
Can prediction markets work without real money?
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -148,6 +155,40 @@ function Contents() {
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3 id="why-is-this-important-">Why is this important?</h3>
|
||||||
|
<p>
|
||||||
|
Prediction markets aggregate and reveal crucial information that would
|
||||||
|
not otherwise be known. They are a bottom-up mechanism that can
|
||||||
|
influence everything from politics, economics, and business, to
|
||||||
|
scientific research and education.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Prediction markets can predict{' '}
|
||||||
|
<a href="https://www.pnas.org/content/112/50/15343">
|
||||||
|
which research papers will replicate
|
||||||
|
</a>
|
||||||
|
; which drug is the most effective; which policy would generate the most
|
||||||
|
tax revenue; which charity will be underfunded; or which startup idea is
|
||||||
|
the most promising. By surfacing and quantifying our collective
|
||||||
|
knowledge, we as a society become wiser.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 id="how-does-betting-work">How does betting work?</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Markets are structured around a question with a binary outcome.</li>
|
||||||
|
<li>
|
||||||
|
Traders can place a bet on either YES or NO. The trader receives some
|
||||||
|
shares of the betting pool. The number of shares depends on the
|
||||||
|
current probability.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
When the market is resolved, the traders who bet on the correct
|
||||||
|
outcome are paid out of the final pool in proportion to the number of
|
||||||
|
shares they own.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3 id="how-are-markets-resolved-">How are markets resolved?</h3>
|
<h3 id="how-are-markets-resolved-">How are markets resolved?</h3>
|
||||||
<p>
|
<p>
|
||||||
The creator of the prediction market decides the outcome and earns{' '}
|
The creator of the prediction market decides the outcome and earns{' '}
|
||||||
|
@ -166,29 +207,9 @@ function Contents() {
|
||||||
or even personal. (E.g. "Will I enjoy participating in the
|
or even personal. (E.g. "Will I enjoy participating in the
|
||||||
Metaverse in 2023?")
|
Metaverse in 2023?")
|
||||||
</p>
|
</p>
|
||||||
<h3 id="why-is-this-important-">Why is this important?</h3>
|
{/* <h3 id="how-is-this-different-from-metaculus-or-hypermind-">
|
||||||
<p>
|
|
||||||
Prediction markets aggregate and reveal crucial information that would
|
|
||||||
not otherwise be known. They are a bottom-up mechanism that can
|
|
||||||
influence everything from politics, economics, and business, to
|
|
||||||
scientific research and education.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Prediction markets can predict{' '}
|
|
||||||
<a href="https://www.pnas.org/content/112/50/15343">
|
|
||||||
which research papers will replicate
|
|
||||||
</a>
|
|
||||||
; which drug is the most effective; which policy would generate the most
|
|
||||||
tax revenue; which charity will be underfunded; or, which startup idea
|
|
||||||
is the most promising.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
By surfacing and quantifying our collective knowledge, we as a society
|
|
||||||
become wiser.
|
|
||||||
</p>
|
|
||||||
<h3 id="how-is-this-different-from-metaculus-or-hypermind-">
|
|
||||||
How is this different from Metaculus or Hypermind?
|
How is this different from Metaculus or Hypermind?
|
||||||
</h3>
|
</h3> */}
|
||||||
{/* <p>
|
{/* <p>
|
||||||
We believe that in order to get the best results, you have to have skin
|
We believe that in order to get the best results, you have to have skin
|
||||||
in the game. We require that people use real money to buy the currency
|
in the game. We require that people use real money to buy the currency
|
||||||
|
@ -199,28 +220,13 @@ function Contents() {
|
||||||
carefully and can't rig the outcome by creating multiple accounts.
|
carefully and can't rig the outcome by creating multiple accounts.
|
||||||
The result is more accurate predictions.
|
The result is more accurate predictions.
|
||||||
</p> */}
|
</p> */}
|
||||||
<p>
|
{/* <p>
|
||||||
Manifold Markets is focused on accessibility and allowing anyone to
|
Manifold Markets is focused on accessibility and allowing anyone to
|
||||||
quickly create and judge a prediction market. When we all have the power
|
quickly create and judge a prediction market. When we all have the power
|
||||||
to create and share prediction markets in seconds and apply our own
|
to create and share prediction markets in seconds and apply our own
|
||||||
judgment on the outcome, it leads to a qualitative shift in the number,
|
judgment on the outcome, it leads to a qualitative shift in the number,
|
||||||
variety, and usefulness of prediction markets.
|
variety, and usefulness of prediction markets.
|
||||||
</p>
|
</p> */}
|
||||||
|
|
||||||
<h3 id="how-does-betting-work">How does betting work?</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Markets are structured around a question with a binary outcome.</li>
|
|
||||||
<li>
|
|
||||||
Traders can place a bet on either YES or NO. The trader receives some
|
|
||||||
shares of the betting pool. The number of shares depends on the
|
|
||||||
current probability.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
When the market is resolved, the traders who bet on the correct
|
|
||||||
outcome are paid out of the final pool in proportion to the number of
|
|
||||||
shares they own.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3 id="type-of-market-maker">What kind of betting system do you use?</h3>
|
<h3 id="type-of-market-maker">What kind of betting system do you use?</h3>
|
||||||
<p>
|
<p>
|
||||||
|
@ -249,6 +255,20 @@ function Contents() {
|
||||||
to find out more!
|
to find out more!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3 id="private-markets">Can I create private markets?</h3>
|
||||||
|
<p>
|
||||||
|
Soon! We're running a pilot version of Manifold for Teams - private
|
||||||
|
Manifold instances where you can discuss internal topics and predict on
|
||||||
|
outcomes for your organization.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If this sounds like something you’d want,{' '}
|
||||||
|
<a href="https://docs.google.com/forms/d/e/1FAIpQLSfM_rxRHemCjKE6KPiYXGyP2nBSInZNKn_wc7yS1-rvlLAVnA/viewform?usp=sf_link">
|
||||||
|
join the waitlist here
|
||||||
|
</a>
|
||||||
|
!
|
||||||
|
</p>
|
||||||
|
|
||||||
<h3 id="who-are-we-">Who are we?</h3>
|
<h3 id="who-are-we-">Who are we?</h3>
|
||||||
<p>Manifold Markets is currently a team of three:</p>
|
<p>Manifold Markets is currently a team of three:</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -296,19 +316,24 @@ function Contents() {
|
||||||
<h2 id="further-reading">Further Reading</h2>
|
<h2 id="further-reading">Further Reading</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://outsidetheasylum.blog/manifold-markets-faq/">
|
||||||
|
An in-depth, unofficial FAQ by Isaac King
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://manifoldmarkets.notion.site/Technical-Guide-to-Manifold-Markets-b9b48a09ea1f45b88d991231171730c5">
|
<a href="https://manifoldmarkets.notion.site/Technical-Guide-to-Manifold-Markets-b9b48a09ea1f45b88d991231171730c5">
|
||||||
Technical Guide to Manifold Markets
|
How Manifold's market maker works
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://astralcodexten.substack.com/p/play-money-and-reputation-systems">
|
||||||
|
Scott Alexander on play-money prediction markets
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://sideways-view.com/2019/10/27/prediction-markets-for-internet-points/">
|
<a href="https://sideways-view.com/2019/10/27/prediction-markets-for-internet-points/">
|
||||||
Paul Christiano: Prediction markets for internet points
|
Paul Christiano on prediction markets for internet points
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://thezvi.wordpress.com/2021/12/02/covid-prediction-markets-at-polymarket/">
|
|
||||||
Zvi Mowshowitz on resolving prediction markets
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,17 +1,121 @@
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { DailyCountChart } from '../components/analytics/charts'
|
||||||
|
import { Col } from '../components/layout/col'
|
||||||
|
import { Spacer } from '../components/layout/spacer'
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
|
import { Title } from '../components/title'
|
||||||
|
import { getDailyBets } from '../lib/firebase/bets'
|
||||||
|
import { getDailyComments } from '../lib/firebase/comments'
|
||||||
|
import { getDailyContracts } from '../lib/firebase/contracts'
|
||||||
|
|
||||||
export default function Analytics() {
|
export async function getStaticProps() {
|
||||||
// Edit dashboard at https://datastudio.google.com/u/0/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3/edit
|
const numberOfDays = 80
|
||||||
|
const today = dayjs(dayjs().format('YYYY-MM-DD'))
|
||||||
|
const startDate = today.subtract(numberOfDays, 'day')
|
||||||
|
|
||||||
|
const [dailyBets, dailyContracts, dailyComments] = await Promise.all([
|
||||||
|
getDailyBets(startDate.valueOf(), numberOfDays),
|
||||||
|
getDailyContracts(startDate.valueOf(), numberOfDays),
|
||||||
|
getDailyComments(startDate.valueOf(), numberOfDays),
|
||||||
|
])
|
||||||
|
|
||||||
|
const dailyBetCounts = dailyBets.map((bets) => bets.length)
|
||||||
|
const dailyContractCounts = dailyContracts.map(
|
||||||
|
(contracts) => contracts.length
|
||||||
|
)
|
||||||
|
const dailyCommentCounts = dailyComments.map((comments) => comments.length)
|
||||||
|
|
||||||
|
const dailyActiveUsers = _.zip(dailyContracts, dailyBets, dailyComments).map(
|
||||||
|
([contracts, bets, comments]) => {
|
||||||
|
const creatorIds = (contracts ?? []).map((c) => c.creatorId)
|
||||||
|
const betUserIds = (bets ?? []).map((bet) => bet.userId)
|
||||||
|
const commentUserIds = (comments ?? []).map((comment) => comment.userId)
|
||||||
|
return _.uniq([...creatorIds, ...betUserIds, commentUserIds]).length
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
startDate: startDate.valueOf(),
|
||||||
|
dailyActiveUsers,
|
||||||
|
dailyBetCounts,
|
||||||
|
dailyContractCounts,
|
||||||
|
dailyCommentCounts,
|
||||||
|
},
|
||||||
|
revalidate: 12 * 60 * 60, // regenerate after half a day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Analytics(props: {
|
||||||
|
startDate: number
|
||||||
|
dailyActiveUsers: number[]
|
||||||
|
dailyBetCounts: number[]
|
||||||
|
dailyContractCounts: number[]
|
||||||
|
dailyCommentCounts: number[]
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<iframe
|
<CustomAnalytics {...props} />
|
||||||
className="w-full"
|
<Spacer h={8} />
|
||||||
height={2200}
|
<FirebaseAnalytics />
|
||||||
src="https://datastudio.google.com/embed/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3"
|
|
||||||
frameBorder="0"
|
|
||||||
style={{ border: 0 }}
|
|
||||||
allowFullScreen
|
|
||||||
></iframe>
|
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CustomAnalytics(props: {
|
||||||
|
startDate: number
|
||||||
|
dailyActiveUsers: number[]
|
||||||
|
dailyBetCounts: number[]
|
||||||
|
dailyContractCounts: number[]
|
||||||
|
dailyCommentCounts: number[]
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
startDate,
|
||||||
|
dailyActiveUsers,
|
||||||
|
dailyBetCounts,
|
||||||
|
dailyContractCounts,
|
||||||
|
dailyCommentCounts,
|
||||||
|
} = props
|
||||||
|
return (
|
||||||
|
<Col>
|
||||||
|
<Title text="Active users" />
|
||||||
|
<DailyCountChart dailyCounts={dailyActiveUsers} startDate={startDate} />
|
||||||
|
|
||||||
|
<Title text="Bets count" />
|
||||||
|
<DailyCountChart
|
||||||
|
dailyCounts={dailyBetCounts}
|
||||||
|
startDate={startDate}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Title text="Markets count" />
|
||||||
|
<DailyCountChart
|
||||||
|
dailyCounts={dailyContractCounts}
|
||||||
|
startDate={startDate}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Title text="Comments count" />
|
||||||
|
<DailyCountChart
|
||||||
|
dailyCounts={dailyCommentCounts}
|
||||||
|
startDate={startDate}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FirebaseAnalytics() {
|
||||||
|
// Edit dashboard at https://datastudio.google.com/u/0/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3/edit
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
className="w-full"
|
||||||
|
height={2200}
|
||||||
|
src="https://datastudio.google.com/embed/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3"
|
||||||
|
frameBorder="0"
|
||||||
|
style={{ border: 0 }}
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
<span className="mb-1">Answer type</span>
|
<span className="mb-1">Answer type</span>
|
||||||
</label>
|
</label>
|
||||||
<Row className="form-control gap-2">
|
<Row className="form-control gap-2">
|
||||||
<label className="cursor-pointer label gap-2">
|
<label className="label cursor-pointer gap-2">
|
||||||
<input
|
<input
|
||||||
className="radio"
|
className="radio"
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -149,7 +149,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
<span className="label-text">Yes / No</span>
|
<span className="label-text">Yes / No</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="cursor-pointer label gap-2">
|
<label className="label cursor-pointer gap-2">
|
||||||
<input
|
<input
|
||||||
className="radio"
|
className="radio"
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -248,6 +248,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
error={anteError}
|
error={anteError}
|
||||||
setError={setAnteError}
|
setError={setAnteError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
contractIdForLoan={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -5,19 +5,17 @@ import { Fold } from '../../../../common/fold'
|
||||||
import { Comment } from '../../../../common/comment'
|
import { Comment } from '../../../../common/comment'
|
||||||
import { Page } from '../../../components/page'
|
import { Page } from '../../../components/page'
|
||||||
import { Title } from '../../../components/title'
|
import { Title } from '../../../components/title'
|
||||||
import {
|
import { Bet, listAllBets } from '../../../lib/firebase/bets'
|
||||||
Bet,
|
|
||||||
getRecentContractBets,
|
|
||||||
listAllBets,
|
|
||||||
} from '../../../lib/firebase/bets'
|
|
||||||
import { listAllComments } from '../../../lib/firebase/comments'
|
|
||||||
import { Contract } from '../../../lib/firebase/contracts'
|
import { Contract } from '../../../lib/firebase/contracts'
|
||||||
import {
|
import {
|
||||||
foldPath,
|
foldPath,
|
||||||
getFoldBySlug,
|
getFoldBySlug,
|
||||||
getFoldContracts,
|
getFoldContracts,
|
||||||
} from '../../../lib/firebase/folds'
|
} from '../../../lib/firebase/folds'
|
||||||
import { ActivityFeed, findActiveContracts } from '../../activity'
|
import {
|
||||||
|
ActivityFeed,
|
||||||
|
findActiveContracts,
|
||||||
|
} from '../../../components/activity-feed'
|
||||||
import { TagsList } from '../../../components/tags-list'
|
import { TagsList } from '../../../components/tags-list'
|
||||||
import { Row } from '../../../components/layout/row'
|
import { Row } from '../../../components/layout/row'
|
||||||
import { UserLink } from '../../../components/user-page'
|
import { UserLink } from '../../../components/user-page'
|
||||||
|
@ -42,6 +40,9 @@ import { useTaggedContracts } from '../../../hooks/use-contracts'
|
||||||
import { Linkify } from '../../../components/linkify'
|
import { Linkify } from '../../../components/linkify'
|
||||||
import { usePropz } from '../../../hooks/use-propz'
|
import { usePropz } from '../../../hooks/use-propz'
|
||||||
import { filterDefined } from '../../../../common/util/array'
|
import { filterDefined } from '../../../../common/util/array'
|
||||||
|
import { useRecentBets } from '../../../hooks/use-bets'
|
||||||
|
import { useRecentComments } from '../../../hooks/use-comments'
|
||||||
|
import { LoadingIndicator } from '../../../components/loading-indicator'
|
||||||
|
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
const { slugs } = props.params
|
const { slugs } = props.params
|
||||||
|
@ -51,42 +52,17 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
|
||||||
const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
|
const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
|
||||||
|
|
||||||
const betsPromise = Promise.all(
|
const bets = await Promise.all(
|
||||||
contracts.map((contract) => listAllBets(contract.id))
|
contracts.map((contract) => listAllBets(contract.id))
|
||||||
)
|
)
|
||||||
|
|
||||||
const [contractComments, contractRecentBets] = await Promise.all([
|
let activeContracts = findActiveContracts(contracts, [], _.flatten(bets))
|
||||||
Promise.all(
|
|
||||||
contracts.map((contract) => listAllComments(contract.id).catch((_) => []))
|
|
||||||
),
|
|
||||||
Promise.all(
|
|
||||||
contracts.map((contract) =>
|
|
||||||
getRecentContractBets(contract.id).catch((_) => [])
|
|
||||||
)
|
|
||||||
),
|
|
||||||
])
|
|
||||||
|
|
||||||
let activeContracts = findActiveContracts(
|
|
||||||
contracts,
|
|
||||||
_.flatten(contractComments),
|
|
||||||
_.flatten(contractRecentBets)
|
|
||||||
)
|
|
||||||
const [resolved, unresolved] = _.partition(
|
const [resolved, unresolved] = _.partition(
|
||||||
activeContracts,
|
activeContracts,
|
||||||
({ isResolved }) => isResolved
|
({ isResolved }) => isResolved
|
||||||
)
|
)
|
||||||
activeContracts = [...unresolved, ...resolved]
|
activeContracts = [...unresolved, ...resolved]
|
||||||
|
|
||||||
const activeContractBets = await Promise.all(
|
|
||||||
activeContracts.map((contract) => listAllBets(contract.id).catch((_) => []))
|
|
||||||
)
|
|
||||||
const activeContractComments = activeContracts.map(
|
|
||||||
(contract) =>
|
|
||||||
contractComments[contracts.findIndex((c) => c.id === contract.id)]
|
|
||||||
)
|
|
||||||
|
|
||||||
const bets = await betsPromise
|
|
||||||
|
|
||||||
const creatorScores = scoreCreators(contracts, bets)
|
const creatorScores = scoreCreators(contracts, bets)
|
||||||
const traderScores = scoreTraders(contracts, bets)
|
const traderScores = scoreTraders(contracts, bets)
|
||||||
const [topCreators, topTraders] = await Promise.all([
|
const [topCreators, topTraders] = await Promise.all([
|
||||||
|
@ -102,8 +78,6 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
curator,
|
curator,
|
||||||
contracts,
|
contracts,
|
||||||
activeContracts,
|
activeContracts,
|
||||||
activeContractBets,
|
|
||||||
activeContractComments,
|
|
||||||
traderScores,
|
traderScores,
|
||||||
topTraders,
|
topTraders,
|
||||||
creatorScores,
|
creatorScores,
|
||||||
|
@ -156,15 +130,8 @@ export default function FoldPage(props: {
|
||||||
creatorScores: {},
|
creatorScores: {},
|
||||||
topCreators: [],
|
topCreators: [],
|
||||||
}
|
}
|
||||||
const {
|
const { curator, traderScores, topTraders, creatorScores, topCreators } =
|
||||||
curator,
|
props
|
||||||
activeContractBets,
|
|
||||||
activeContractComments,
|
|
||||||
traderScores,
|
|
||||||
topTraders,
|
|
||||||
creatorScores,
|
|
||||||
topCreators,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { slugs } = router.query as { slugs: string[] }
|
const { slugs } = router.query as { slugs: string[] }
|
||||||
|
@ -190,6 +157,9 @@ export default function FoldPage(props: {
|
||||||
props.activeContracts.map((contract) => contractsMap[contract.id])
|
props.activeContracts.map((contract) => contractsMap[contract.id])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const recentBets = useRecentBets()
|
||||||
|
const recentComments = useRecentComments()
|
||||||
|
|
||||||
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
|
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
@ -272,19 +242,24 @@ export default function FoldPage(props: {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{page === 'activity' ? (
|
{page === 'activity' ? (
|
||||||
<>
|
recentBets && recentComments ? (
|
||||||
<ActivityFeed
|
<>
|
||||||
contracts={activeContracts}
|
<ActivityFeed
|
||||||
contractBets={activeContractBets}
|
contracts={activeContracts}
|
||||||
contractComments={activeContractComments}
|
recentBets={recentBets ?? []}
|
||||||
/>
|
recentComments={recentComments ?? []}
|
||||||
{activeContracts.length === 0 && (
|
loadBetAndCommentHistory
|
||||||
<div className="mx-2 mt-4 text-gray-500 lg:mx-0">
|
/>
|
||||||
No activity from matching markets.{' '}
|
{activeContracts.length === 0 && (
|
||||||
{isCurator && 'Try editing to add more tags!'}
|
<div className="mx-2 mt-4 text-gray-500 lg:mx-0">
|
||||||
</div>
|
No activity from matching markets.{' '}
|
||||||
)}
|
{isCurator && 'Try editing to add more tags!'}
|
||||||
</>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<LoadingIndicator className="mt-4" />
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<SearchableGrid
|
<SearchableGrid
|
||||||
contracts={contracts}
|
contracts={contracts}
|
||||||
|
|
|
@ -6,9 +6,8 @@ import _ from 'lodash'
|
||||||
|
|
||||||
import { Contract } from '../lib/firebase/contracts'
|
import { Contract } from '../lib/firebase/contracts'
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
import { ActivityFeed, SummaryActivityFeed } from './activity'
|
import { ActivityFeed, SummaryActivityFeed } from '../components/activity-feed'
|
||||||
import { Comment } from '../lib/firebase/comments'
|
import { Comment } from '../lib/firebase/comments'
|
||||||
import { Bet } from '../lib/firebase/bets'
|
|
||||||
import FeedCreate from '../components/feed-create'
|
import FeedCreate from '../components/feed-create'
|
||||||
import { Spacer } from '../components/layout/spacer'
|
import { Spacer } from '../components/layout/spacer'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
|
@ -23,10 +22,11 @@ import {
|
||||||
useFilterYourContracts,
|
useFilterYourContracts,
|
||||||
useFindActiveContracts,
|
useFindActiveContracts,
|
||||||
} from '../hooks/use-find-active-contracts'
|
} from '../hooks/use-find-active-contracts'
|
||||||
import { useGetRecentBets } from '../hooks/use-bets'
|
|
||||||
import { usePropz } from '../hooks/use-propz'
|
import { usePropz } from '../hooks/use-propz'
|
||||||
import { useActiveContracts } from '../hooks/use-contracts'
|
|
||||||
import { IS_PRIVATE_MANIFOLD } from '../lib/firebase/init'
|
import { IS_PRIVATE_MANIFOLD } from '../lib/firebase/init'
|
||||||
|
import { useGetRecentBets, useRecentBets } from '../hooks/use-bets'
|
||||||
|
import { useActiveContracts } from '../hooks/use-contracts'
|
||||||
|
import { useRecentComments } from '../hooks/use-comments'
|
||||||
|
|
||||||
export async function getStaticPropz() {
|
export async function getStaticPropz() {
|
||||||
const contractInfo = await getAllContractInfo()
|
const contractInfo = await getAllContractInfo()
|
||||||
|
@ -40,7 +40,6 @@ export async function getStaticPropz() {
|
||||||
const Home = (props: {
|
const Home = (props: {
|
||||||
contracts: Contract[]
|
contracts: Contract[]
|
||||||
folds: Fold[]
|
folds: Fold[]
|
||||||
recentBets: Bet[]
|
|
||||||
recentComments: Comment[]
|
recentComments: Comment[]
|
||||||
}) => {
|
}) => {
|
||||||
props = usePropz(getStaticPropz) ?? {
|
props = usePropz(getStaticPropz) ?? {
|
||||||
|
@ -48,7 +47,7 @@ const Home = (props: {
|
||||||
folds: [],
|
folds: [],
|
||||||
recentComments: [],
|
recentComments: [],
|
||||||
}
|
}
|
||||||
const { folds, recentComments } = props
|
const { folds } = props
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const contracts = useActiveContracts() ?? props.contracts
|
const contracts = useActiveContracts() ?? props.contracts
|
||||||
|
@ -58,13 +57,15 @@ const Home = (props: {
|
||||||
contracts
|
contracts
|
||||||
)
|
)
|
||||||
|
|
||||||
const recentBets = useGetRecentBets()
|
const initialRecentBets = useGetRecentBets()
|
||||||
const { activeContracts, activeBets, activeComments } =
|
const recentBets = useRecentBets() ?? initialRecentBets
|
||||||
useFindActiveContracts({
|
const recentComments = useRecentComments() ?? props.recentComments
|
||||||
contracts: yourContracts,
|
|
||||||
recentBets: recentBets ?? [],
|
const { activeContracts } = useFindActiveContracts({
|
||||||
recentComments,
|
contracts: yourContracts,
|
||||||
})
|
recentBets: initialRecentBets ?? [],
|
||||||
|
recentComments: props.recentComments,
|
||||||
|
})
|
||||||
|
|
||||||
const exploreContracts = useExploreContracts()
|
const exploreContracts = useExploreContracts()
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ const Home = (props: {
|
||||||
return (
|
return (
|
||||||
<Page assertUser="signed-in">
|
<Page assertUser="signed-in">
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="w-full max-w-3xl">
|
<Col className="w-full max-w-[700px]">
|
||||||
<FeedCreate user={user ?? undefined} />
|
<FeedCreate user={user ?? undefined} />
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
@ -93,7 +94,7 @@ const Home = (props: {
|
||||||
|
|
||||||
<Spacer h={5} />
|
<Spacer h={5} />
|
||||||
|
|
||||||
<Col className="mx-3 mb-3 gap-2 text-sm text-gray-800 sm:flex-row">
|
<Col className="mb-3 gap-2 text-sm text-gray-800 sm:flex-row">
|
||||||
<Row className="gap-2">
|
<Row className="gap-2">
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
<div
|
<div
|
||||||
|
@ -124,8 +125,8 @@ const Home = (props: {
|
||||||
(recentBets ? (
|
(recentBets ? (
|
||||||
<ActivityFeed
|
<ActivityFeed
|
||||||
contracts={activeContracts}
|
contracts={activeContracts}
|
||||||
contractBets={activeBets}
|
recentBets={recentBets}
|
||||||
contractComments={activeComments}
|
recentComments={recentComments}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LoadingIndicator className="mt-4" />
|
<LoadingIndicator className="mt-4" />
|
||||||
|
|
|
@ -245,6 +245,7 @@ ${TEST_VALUE}
|
||||||
error={anteError}
|
error={anteError}
|
||||||
setError={setAnteError}
|
setError={setAnteError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
contractIdForLoan={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user