Merge branch 'main' into theoremone

This commit is contained in:
Austin Chen 2022-03-07 13:39:00 -08:00
commit 5d64d53c65
43 changed files with 988 additions and 451 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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 }))
}

View File

@ -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]

View File

@ -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,

View File

@ -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, {

View File

@ -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 ||

View File

@ -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, {

View File

@ -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,

View File

@ -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)
}) })
} }

View File

@ -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)
} }

View File

@ -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>
)
}

View File

@ -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>
)} )}

View 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>
)
}

View File

@ -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)}
&nbsp; <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!

View File

@ -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">

View File

@ -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}

View File

@ -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>

View File

@ -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)}
&nbsp; <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'
)} )}

View File

@ -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">
&nbsp; <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!

View File

@ -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>
) )

View 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
}

View File

@ -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>
))} ))}
</> </>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>
</> </>
) )
} }

View File

@ -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' },

View File

@ -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>

View File

@ -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 = (

View File

@ -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'

View File

@ -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>()

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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} />

View File

@ -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. &quot;Will I enjoy participating in the or even personal. (E.g. &quot;Will I enjoy participating in the
Metaverse in 2023?&quot;) Metaverse in 2023?&quot;)
</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&#39;t rig the outcome by creating multiple accounts. carefully and can&#39;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 youd 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>

View File

@ -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
/>
)
}

View File

@ -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>

View File

@ -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}

View File

@ -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" />

View File

@ -245,6 +245,7 @@ ${TEST_VALUE}
error={anteError} error={anteError}
setError={setAnteError} setError={setAnteError}
disabled={isSubmitting} disabled={isSubmitting}
contractIdForLoan={undefined}
/> />
</div> </div>