Merge branch 'main' into cpmm

This commit is contained in:
mantikoros 2022-03-07 11:30:26 -06:00
commit 14ee9735c7
27 changed files with 631 additions and 279 deletions

View File

@ -40,9 +40,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)
@ -58,6 +55,10 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
) )
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet) 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)
@ -70,8 +71,6 @@ export const placeBet = 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 } =
outcomeType === 'BINARY' outcomeType === 'BINARY'
? mechanism === 'dpm-2' ? mechanism === 'dpm-2'

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

@ -14,7 +14,7 @@ export function AmountInput(props: {
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
contractId: string | undefined contractIdForLoan: string | undefined
minimumAmount?: number minimumAmount?: number
disabled?: boolean disabled?: boolean
className?: string className?: string
@ -27,7 +27,7 @@ export function AmountInput(props: {
onChange, onChange,
error, error,
setError, setError,
contractId, contractIdForLoan,
disabled, disabled,
className, className,
inputClassName, inputClassName,
@ -37,14 +37,13 @@ export function AmountInput(props: {
const user = useUser() const user = useUser()
const userBets = useUserContractBets(user?.id, contractId) ?? [] const userBets = useUserContractBets(user?.id, contractIdForLoan) ?? []
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale) const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0) const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = Math.min( const loanAmount = contractIdForLoan
amount ?? 0, ? Math.min(amount ?? 0, MAX_LOAN_PER_CONTRACT - prevLoanAmount)
MAX_LOAN_PER_CONTRACT - prevLoanAmount : 0
)
const onAmountChange = (str: string) => { const onAmountChange = (str: string) => {
if (str.includes('-')) { if (str.includes('-')) {
@ -58,7 +57,12 @@ export function AmountInput(props: {
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))
@ -99,7 +103,7 @@ export function AmountInput(props: {
)} )}
{user && ( {user && (
<Col className="gap-3 text-sm"> <Col className="gap-3 text-sm">
{contractId && ( {contractIdForLoan && (
<Row className="items-center justify-between gap-2 text-gray-500"> <Row className="items-center justify-between gap-2 text-gray-500">
<Row className="items-center gap-2"> <Row className="items-center gap-2">
Amount loaned{' '} Amount loaned{' '}

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

@ -49,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)
@ -103,11 +98,11 @@ export function AnswerBetPanel(props: {
return ( return (
<Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}> <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>
@ -119,10 +114,10 @@ export function AnswerBetPanel(props: {
setError={setError} setError={setError}
disabled={isSubmitting} disabled={isSubmitting}
inputRef={inputRef} inputRef={inputRef}
contractId={contract.id} contractIdForLoan={contract.id}
/> />
<Col className="gap-3 mt-3 w-full"> <Col className="mt-3 w-full gap-3">
<Row className="justify-between items-center text-sm"> <Row className="items-center justify-between text-sm">
<div className="text-gray-500">Probability</div> <div className="text-gray-500">Probability</div>
<Row> <Row>
<div>{formatPercent(initialProb)}</div> <div>{formatPercent(initialProb)}</div>
@ -131,8 +126,8 @@ export function AnswerBetPanel(props: {
</Row> </Row>
</Row> </Row>
<Row className="justify-between items-start text-sm gap-2"> <Row className="items-start justify-between gap-2 text-sm">
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500"> <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
<div>Payout if chosen</div> <div>Payout if chosen</div>
<InfoTooltip <InfoTooltip
text={`Current payout for ${formatWithCommas( text={`Current payout for ${formatWithCommas(
@ -142,7 +137,7 @@ export function AnswerBetPanel(props: {
)} shares`} )} shares`}
/> />
</Row> </Row>
<Row className="flex-wrap justify-end items-end gap-2"> <Row className="flex-wrap items-end justify-end gap-2">
<span className="whitespace-nowrap"> <span className="whitespace-nowrap">
{formatMoney(currentPayout)} {formatMoney(currentPayout)}
</span> </span>

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

@ -98,9 +98,9 @@ export function AnswersPanel(props: {
))} ))}
{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(getDpmOutcomeProbability(contract.totalShares, '0'))} {formatPercent(getDpmOutcomeProbability(contract.totalShares, '0'))}
</div> </div>

View File

@ -38,6 +38,7 @@ export function CreateAnswerPanel(props: {
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,
@ -50,7 +51,7 @@ export function CreateAnswerPanel(props: {
setText('') setText('')
setBetAmount(10) setBetAmount(10)
setAmountError(undefined) setAmountError(undefined)
} } else setAmountError(result.message)
} }
} }
@ -74,7 +75,7 @@ export function CreateAnswerPanel(props: {
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
@ -88,14 +89,14 @@ export function CreateAnswerPanel(props: {
<div /> <div />
<Col <Col
className={clsx( className={clsx(
'sm:flex-row sm:items-end 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}
@ -103,11 +104,11 @@ export function CreateAnswerPanel(props: {
setError={setAmountError} setError={setAmountError}
minimumAmount={1} minimumAmount={1}
disabled={isSubmitting} disabled={isSubmitting}
contractId={contract.id} contractIdForLoan={contract.id}
/> />
</Col> </Col>
<Col className="gap-3"> <Col className="gap-3">
<Row className="justify-between items-center text-sm"> <Row className="items-center justify-between text-sm">
<div className="text-gray-500">Probability</div> <div className="text-gray-500">Probability</div>
<Row> <Row>
<div>{formatPercent(0)}</div> <div>{formatPercent(0)}</div>
@ -116,8 +117,8 @@ export function CreateAnswerPanel(props: {
</Row> </Row>
</Row> </Row>
<Row className="justify-between text-sm gap-2"> <Row className="justify-between gap-2 text-sm">
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500"> <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
<div>Payout if chosen</div> <div>Payout if chosen</div>
<InfoTooltip <InfoTooltip
text={`Current payout for ${formatWithCommas( text={`Current payout for ${formatWithCommas(
@ -125,7 +126,7 @@ export function CreateAnswerPanel(props: {
)} / ${formatWithCommas(shares)} shares`} )} / ${formatWithCommas(shares)} shares`}
/> />
</Row> </Row>
<Row className="flex-wrap justify-end items-end gap-2"> <Row className="flex-wrap items-end justify-end gap-2">
<span className="whitespace-nowrap"> <span className="whitespace-nowrap">
{formatMoney(currentPayout)} {formatMoney(currentPayout)}
</span> </span>

View File

@ -68,11 +68,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)
@ -157,11 +152,11 @@ export function BetPanel(props: {
setError={setError} setError={setError}
disabled={isSubmitting} disabled={isSubmitting}
inputRef={inputRef} inputRef={inputRef}
contractId={contract.id} contractIdForLoan={contract.id}
/> />
<Col className="gap-3 mt-3 w-full"> <Col className="mt-3 w-full gap-3">
<Row className="justify-between items-center text-sm"> <Row className="items-center justify-between text-sm">
<div className="text-gray-500">Probability</div> <div className="text-gray-500">Probability</div>
<Row> <Row>
<div>{formatPercent(initialProb)}</div> <div>{formatPercent(initialProb)}</div>
@ -170,15 +165,15 @@ export function BetPanel(props: {
</Row> </Row>
</Row> </Row>
<Row className="justify-between items-start text-sm gap-2"> <Row className="items-start justify-between gap-2 text-sm">
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500"> <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
<div> <div>
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} /> Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
</div> </div>
{tooltip && <InfoTooltip text={tooltip} />} {tooltip && <InfoTooltip text={tooltip} />}
</Row> </Row>
<Row className="flex-wrap justify-end items-end gap-2"> <Row className="flex-wrap items-end justify-end gap-2">
<span className="whitespace-nowrap"> <span className="whitespace-nowrap">
{formatMoney(currentPayout)} {formatMoney(currentPayout)}
</span> </span>

View File

@ -127,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 (
@ -136,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(currentInvestment)}{' '} {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>
@ -527,6 +527,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
const outcomeProb = getProbabilityAfterSale(contract, outcome, shares) const outcomeProb = getProbabilityAfterSale(contract, outcome, shares)
const saleAmount = calculateSaleAmount(contract, bet) const saleAmount = calculateSaleAmount(contract, bet)
const profit = saleAmount - bet.amount
return ( return (
<ConfirmationButton <ConfirmationButton
@ -554,6 +555,8 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
)} )}
<div className="mt-2 mb-1 text-sm"> <div className="mt-2 mb-1 text-sm">
{profit > 0 ? 'Profit' : 'Loss'}: {formatMoney(profit).replace('-', '')}
<br />
Market probability: {formatPercent(initialProb)} {' '} Market probability: {formatPercent(initialProb)} {' '}
{formatPercent(outcomeProb)} {formatPercent(outcomeProb)}
</div> </div>

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

@ -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'
@ -136,6 +136,7 @@ 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 || e.metaKey)) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
submitComment() submitComment()
@ -290,7 +291,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">
@ -301,8 +305,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)
@ -337,22 +344,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="self-end sm:self-start text-sm relative top-4"
>
<div className="text-gray-500 pb-1.5">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>
</> </>
) )
@ -684,6 +703,7 @@ type ActivityItem = {
| 'close' | 'close'
| 'resolve' | 'resolve'
| 'expand' | 'expand'
| undefined
} }
type FeedType = type FeedType =
@ -694,64 +714,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"
@ -795,6 +775,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
@ -808,7 +899,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

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

@ -32,7 +32,7 @@ function FollowFoldButton(props: {
className={clsx( className={clsx(
'rounded-full border-2 px-4 py-1 shadow-md', 'rounded-full border-2 px-4 py-1 shadow-md',
'cursor-pointer', 'cursor-pointer',
followed ? 'bg-gray-300 border-gray-300' : 'bg-white' followed ? 'border-gray-300 bg-gray-300' : 'bg-white'
)} )}
onClick={onClick} onClick={onClick}
> >
@ -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 rounded bg-white p-4 shadow-md cursor-text',
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

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

@ -213,3 +213,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

@ -116,7 +116,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 ?? []}
@ -143,7 +143,7 @@ export default function ContractPage(props: {
<> <>
<div className="md:ml-6" /> <div className="md:ml-6" />
<Col className="flex-1"> <Col className="md:w-[310px] flex-shrink-0">
{allowTrade && ( {allowTrade && (
<BetPanel className="hidden lg:flex" contract={contract} /> <BetPanel className="hidden lg:flex" contract={contract} />
)} )}

View File

@ -19,10 +19,10 @@ export default function AddFundsPage() {
<SEO title="Add funds" description="Add funds" url="/add-funds" /> <SEO title="Add funds" description="Add funds" url="/add-funds" />
<Col className="items-center"> <Col className="items-center">
<Col className="bg-white rounded sm:shadow-md p-4 py-8 sm:p-8 h-full"> <Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
<Title className="!mt-0" text="Get Manifold Dollars" /> <Title className="!mt-0" text="Get Manifold Dollars" />
<img <img
className="mb-6 block self-center -scale-x-100" className="mb-6 block -scale-x-100 self-center"
src="/stylized-crane-black.png" src="/stylized-crane-black.png"
width={200} width={200}
height={200} height={200}

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,7 +248,7 @@ export function NewContract(props: { question: string; tag?: string }) {
error={anteError} error={anteError}
setError={setAnteError} setError={setAnteError}
disabled={isSubmitting} disabled={isSubmitting}
contractId={undefined} contractIdForLoan={undefined}
/> />
</div> </div>

View File

@ -12,7 +12,10 @@ import {
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'
@ -36,6 +39,9 @@ import { SEO } from '../../../components/SEO'
import { useTaggedContracts } from '../../../hooks/use-contracts' import { useTaggedContracts } from '../../../hooks/use-contracts'
import { Linkify } from '../../../components/linkify' import { Linkify } from '../../../components/linkify'
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 getStaticProps(props: { params: { slugs: string[] } }) { export async function getStaticProps(props: { params: { slugs: string[] } }) {
const { slugs } = props.params const { slugs } = props.params
@ -48,7 +54,6 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
const bets = await Promise.all( const bets = await Promise.all(
contracts.map((contract) => listAllBets(contract.id)) contracts.map((contract) => listAllBets(contract.id))
) )
const betsByContract = _.fromPairs(contracts.map((c, i) => [c.id, bets[i]]))
let activeContracts = findActiveContracts(contracts, [], _.flatten(bets)) let activeContracts = findActiveContracts(contracts, [], _.flatten(bets))
const [resolved, unresolved] = _.partition( const [resolved, unresolved] = _.partition(
@ -57,10 +62,6 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
) )
activeContracts = [...unresolved, ...resolved] activeContracts = [...unresolved, ...resolved]
const activeContractBets = activeContracts.map(
(contract) => betsByContract[contract.id] ?? []
)
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([
@ -76,8 +77,6 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
curator, curator,
contracts, contracts,
activeContracts, activeContracts,
activeContractBets,
activeContractComments: activeContracts.map(() => []),
traderScores, traderScores,
topTraders, topTraders,
creatorScores, creatorScores,
@ -117,15 +116,8 @@ export default function FoldPage(props: {
creatorScores: { [userId: string]: number } creatorScores: { [userId: string]: number }
topCreators: User[] topCreators: User[]
}) { }) {
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[] }
@ -151,6 +143,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 />
} }
@ -233,19 +228,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,8 +22,9 @@ import {
useFilterYourContracts, useFilterYourContracts,
useFindActiveContracts, useFindActiveContracts,
} from '../hooks/use-find-active-contracts' } from '../hooks/use-find-active-contracts'
import { useGetRecentBets } from '../hooks/use-bets' import { useGetRecentBets, useRecentBets } from '../hooks/use-bets'
import { useActiveContracts } from '../hooks/use-contracts' import { useActiveContracts } from '../hooks/use-contracts'
import { useRecentComments } from '../hooks/use-comments'
export async function getStaticProps() { export async function getStaticProps() {
const contractInfo = await getAllContractInfo() const contractInfo = await getAllContractInfo()
@ -38,10 +38,9 @@ export async function getStaticProps() {
const Home = (props: { const Home = (props: {
contracts: Contract[] contracts: Contract[]
folds: Fold[] folds: Fold[]
recentBets: Bet[]
recentComments: Comment[] recentComments: Comment[]
}) => { }) => {
const { folds, recentComments } = props const { folds } = props
const user = useUser() const user = useUser()
const contracts = useActiveContracts() ?? props.contracts const contracts = useActiveContracts() ?? props.contracts
@ -51,13 +50,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()
@ -71,7 +72,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} />
@ -85,7 +86,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
@ -116,8 +117,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

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