From 41b5dd2e381cd915681eb52d14523e7d1067451c Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 1 Mar 2022 17:25:19 -0800 Subject: [PATCH 01/31] Don't send answer emails if the submitter is also the market creator --- functions/src/emails.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 00696186..0ded7b7d 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -223,6 +223,10 @@ export const sendNewAnswerEmail = async ( ) => { // Send to just the creator for now. const { creatorId: userId } = contract + + // Don't send the creator's own answers. + if (answer.userId === userId) return + const privateUser = await getPrivateUser(userId) if ( !privateUser || From 5944cada771f48cddc1f6a3ab34b4d744132fe9b Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 1 Mar 2022 17:43:06 -0800 Subject: [PATCH 02/31] Quick fix: don't show points in graph when > 100 bets --- web/components/contract-prob-graph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract-prob-graph.tsx b/web/components/contract-prob-graph.tsx index 23daabfb..d764179c 100644 --- a/web/components/contract-prob-graph.tsx +++ b/web/components/contract-prob-graph.tsx @@ -83,7 +83,7 @@ export function ContractProbGraph(props: { contract: Contract; bets: Bet[] }) { format: (time) => formatTime(+time, lessThanAWeek), }} colors={{ datum: 'color' }} - pointSize={10} + pointSize={bets.length > 100 ? 0 : 10} pointBorderWidth={1} pointBorderColor="#fff" enableSlices="x" From 6243f132aaf07302d63ec64525458f90d477a12e Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 1 Mar 2022 18:00:14 -0800 Subject: [PATCH 03/31] Add "closed" sort option --- web/components/contract-card.tsx | 5 +++-- web/components/contracts-list.tsx | 11 +++++++---- web/hooks/use-sort-and-query-params.tsx | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index 55bf303a..36bfd26b 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -150,7 +150,8 @@ function AbbrContractDetails(props: { ) : showCloseTime ? ( - Closes {fromNow(closeTime || 0)} + {(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '} + {fromNow(closeTime || 0)} ) : ( @@ -312,7 +313,7 @@ function EditableCloseDate(props: { className="btn btn-xs btn-ghost" onClick={() => setIsEditingCloseTime(true)} > - Edit + Edit ))} diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx index 911e546e..d64df8d8 100644 --- a/web/components/contracts-list.tsx +++ b/web/components/contracts-list.tsx @@ -229,11 +229,13 @@ export function SearchableGrid(props: { ) } else if (sort === 'oldest') { 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, (contract) => contract.closeTime) - // Hide contracts that have already closed - matches = matches.filter(({ closeTime }) => (closeTime || 0) > Date.now()) + const hideClosed = sort === 'closed' + matches = matches.filter( + ({ closeTime }) => closeTime && closeTime > Date.now() !== hideClosed + ) } else if (sort === 'most-traded') { matches.sort( (a, b) => contractMetrics(b).truePool - contractMetrics(a).truePool @@ -272,6 +274,7 @@ export function SearchableGrid(props: { + @@ -289,7 +292,7 @@ export function SearchableGrid(props: { ) : ( )} diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index 2270fa4a..3eca5f3c 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -10,6 +10,7 @@ export type Sort = | 'most-traded' | '24-hour-vol' | 'close-date' + | 'closed' | 'resolved' | 'all' From ce30b34480d2614290a2afbfff064b15aa7016c0 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 1 Mar 2022 18:08:44 -0800 Subject: [PATCH 04/31] Fetch somewhat less data for fold static props --- web/pages/fold/[...slugs]/index.tsx | 39 ++++++----------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx index f1df850c..1d77444b 100644 --- a/web/pages/fold/[...slugs]/index.tsx +++ b/web/pages/fold/[...slugs]/index.tsx @@ -5,12 +5,7 @@ import { Fold } from '../../../../common/fold' import { Comment } from '../../../../common/comment' import { Page } from '../../../components/page' import { Title } from '../../../components/title' -import { - Bet, - getRecentContractBets, - listAllBets, -} from '../../../lib/firebase/bets' -import { listAllComments } from '../../../lib/firebase/comments' +import { Bet, listAllBets } from '../../../lib/firebase/bets' import { Contract } from '../../../lib/firebase/contracts' import { foldPath, @@ -50,41 +45,21 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) { const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : [] - const betsPromise = Promise.all( + const bets = await Promise.all( contracts.map((contract) => listAllBets(contract.id)) ) + const betsByContract = _.fromPairs(contracts.map((c, i) => [c.id, bets[i]])) - const [contractComments, contractRecentBets] = await Promise.all([ - 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) - ) + let activeContracts = findActiveContracts(contracts, [], _.flatten(bets)) const [resolved, unresolved] = _.partition( activeContracts, ({ isResolved }) => isResolved ) activeContracts = [...unresolved, ...resolved] - const activeContractBets = await Promise.all( - activeContracts.map((contract) => listAllBets(contract.id).catch((_) => [])) + const activeContractBets = activeContracts.map( + (contract) => betsByContract[contract.id] ?? [] ) - const activeContractComments = activeContracts.map( - (contract) => - contractComments[contracts.findIndex((c) => c.id === contract.id)] - ) - - const bets = await betsPromise const creatorScores = scoreCreators(contracts, bets) const traderScores = scoreTraders(contracts, bets) @@ -102,7 +77,7 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) { contracts, activeContracts, activeContractBets, - activeContractComments, + activeContractComments: activeContracts.map(() => []), traderScores, topTraders, creatorScores, From a3973b3481fc8cd46187cd26505fab6b5ad659c3 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 1 Mar 2022 18:52:09 -0800 Subject: [PATCH 05/31] Comment on cmd+enter too --- web/components/contract-feed.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index c582a043..5a3be62f 100644 --- a/web/components/contract-feed.tsx +++ b/web/components/contract-feed.tsx @@ -136,7 +136,7 @@ function FeedBet(props: { activityItem: any; feedType: FeedType }) { placeholder="Add a comment..." rows={3} onKeyDown={(e) => { - if (e.key === 'Enter' && e.ctrlKey) { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { submitComment() } }} @@ -181,7 +181,7 @@ function EditContract(props: { e.target.setSelectionRange(text.length, text.length) } onKeyDown={(e) => { - if (e.key === 'Enter' && e.ctrlKey) { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { onSave(text) } }} From 985cdd25379aa69773fdfc0010ce343933c3ea74 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 1 Mar 2022 21:31:48 -0600 Subject: [PATCH 06/31] =?UTF-8?q?=F0=9F=8F=A6=20Per-market=20loans!=20(#57?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Loan backend: Add loanAmount field to Bet, manage loans up to max loan amount per market -- buy, sell, and resolve. * Loan frontend: show your loan amount in bet panel, answer bet panel * Resolve emails include full payout not subtracting loan * Exclude sold bets from current loan amount * Handle bets table for loans. Sell dialog explains how you will repay your loan. * Floor remaining balance * Fix layout of create answer bet info * Clean up Sell popup UI * Fix bug where listen query was not updating data. * Reword loan copy * Adjust bet panel width * Fix loan calc on front end * Add comment for includeMetadataChanges. Co-authored-by: Austin Chen --- common/bet.ts | 3 + common/new-bet.ts | 21 ++++++- common/payouts.ts | 9 +++ common/sell-bet.ts | 4 +- functions/src/create-answer.ts | 19 +++++- functions/src/place-bet.ts | 25 +++++++- functions/src/resolve-market.ts | 20 +++++- web/components/amount-input.tsx | 56 ++++++++++++++--- web/components/answers/answer-bet-panel.tsx | 63 ++++++++++--------- web/components/answers/answer-item.tsx | 1 + .../answers/create-answer-panel.tsx | 46 ++++++++------ web/components/bet-panel.tsx | 53 ++++++++-------- web/components/bets-list.tsx | 39 +++++++----- web/hooks/use-user-bets.ts | 20 +++++- web/lib/firebase/bets.ts | 15 +++++ web/lib/firebase/utils.ts | 9 ++- web/pages/[username]/[contractSlug].tsx | 2 +- web/pages/create.tsx | 1 + web/pages/make-predictions.tsx | 1 + 19 files changed, 291 insertions(+), 116 deletions(-) diff --git a/common/bet.ts b/common/bet.ts index 7da4b18c..a3e8e714 100644 --- a/common/bet.ts +++ b/common/bet.ts @@ -4,6 +4,7 @@ export type Bet = { contractId: string amount: number // bet size; negative if SELL bet + loanAmount?: number outcome: string shares: number // dynamic parimutuel pool weight; negative if SELL bet @@ -21,3 +22,5 @@ export type Bet = { createdTime: number } + +export const MAX_LOAN_PER_CONTRACT = 20 diff --git a/common/new-bet.ts b/common/new-bet.ts index 29fd421a..0e94c85a 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -1,4 +1,5 @@ -import { Bet } from './bet' +import * as _ from 'lodash' +import { Bet, MAX_LOAN_PER_CONTRACT } from './bet' import { calculateShares, getProbability, @@ -11,6 +12,7 @@ export const getNewBinaryBetInfo = ( user: User, outcome: 'YES' | 'NO', amount: number, + loanAmount: number, contract: Contract, newBetId: string ) => { @@ -45,6 +47,7 @@ export const getNewBinaryBetInfo = ( userId: user.id, contractId: contract.id, amount, + loanAmount, shares, outcome, probBefore, @@ -52,7 +55,7 @@ export const getNewBinaryBetInfo = ( createdTime: Date.now(), } - const newBalance = user.balance - amount + const newBalance = user.balance - (amount - loanAmount) return { newBet, newPool, newTotalShares, newTotalBets, newBalance } } @@ -61,6 +64,7 @@ export const getNewMultiBetInfo = ( user: User, outcome: string, amount: number, + loanAmount: number, contract: Contract, newBetId: string ) => { @@ -85,6 +89,7 @@ export const getNewMultiBetInfo = ( userId: user.id, contractId: contract.id, amount, + loanAmount, shares, outcome, probBefore, @@ -92,7 +97,17 @@ export const getNewMultiBetInfo = ( createdTime: Date.now(), } - const newBalance = user.balance - amount + const newBalance = user.balance - (amount - loanAmount) 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 +} diff --git a/common/payouts.ts b/common/payouts.ts index 5c29d6a9..446b75d7 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -161,3 +161,12 @@ export const getPayoutsMultiOutcome = ( .map(({ userId, payout }) => ({ userId, payout })) .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 })) +} diff --git a/common/sell-bet.ts b/common/sell-bet.ts index cc824386..1b3b133d 100644 --- a/common/sell-bet.ts +++ b/common/sell-bet.ts @@ -11,7 +11,7 @@ export const getSellBetInfo = ( newBetId: string ) => { 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) @@ -57,7 +57,7 @@ export const getSellBetInfo = ( }, } - const newBalance = user.balance + saleAmount + const newBalance = user.balance + saleAmount - (loanAmount ?? 0) return { newBet, diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index 5e711e03..f192fc7e 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -3,10 +3,11 @@ import * as admin from 'firebase-admin' import { Contract } from '../../common/contract' import { User } from '../../common/user' -import { getNewMultiBetInfo } from '../../common/new-bet' +import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet' import { Answer } from '../../common/answer' import { getContract, getValues } from './utils' import { sendNewAnswerEmail } from './emails' +import { Bet } from '../../common/bet' export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -55,6 +56,11 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( if (closeTime && Date.now() > closeTime) 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( firestore .collection(`contracts/${contractId}/answers`) @@ -92,8 +98,17 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( .collection(`contracts/${contractId}/bets`) .doc() + const loanAmount = getLoanAmount(yourBets, amount) + 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.update(contractDoc, { diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index e37dc12c..4adb1779 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -3,7 +3,13 @@ import * as admin from 'firebase-admin' import { Contract } from '../../common/contract' 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' +import { getValues } from './utils' export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -46,6 +52,11 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( if (closeTime && Date.now() > closeTime) 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) + if (outcomeType === 'FREE_RESPONSE') { const answerSnap = await transaction.get( contractDoc.collection('answers').doc(outcome) @@ -58,16 +69,26 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( .collection(`contracts/${contractId}/bets`) .doc() + const loanAmount = getLoanAmount(yourBets, amount) + const { newBet, newPool, newTotalShares, newTotalBets, newBalance } = outcomeType === 'BINARY' ? getNewBinaryBetInfo( user, outcome as 'YES' | 'NO', amount, + loanAmount, + contract, + newBetDoc.id + ) + : getNewMultiBetInfo( + user, + outcome, + amount, + loanAmount, contract, newBetDoc.id ) - : getNewMultiBetInfo(user, outcome, amount, contract, newBetDoc.id) transaction.create(newBetDoc, newBet) transaction.update(contractDoc, { diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 5da5b272..9e3746a2 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -7,7 +7,11 @@ import { User } from '../../common/user' import { Bet } from '../../common/bet' import { getUser, payUser } from './utils' import { sendMarketResolutionEmail } from './emails' -import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts' +import { + getLoanPayouts, + getPayouts, + getPayoutsMultiOutcome, +} from '../../common/payouts' import { removeUndefinedProps } from '../../common/util/object' export const resolveMarket = functions @@ -99,13 +103,23 @@ export const resolveMarket = functions ? getPayoutsMultiOutcome(resolutions, contract, openBets) : getPayouts(outcome, contract, openBets, resolutionProbability) + const loanPayouts = getLoanPayouts(openBets) + console.log('payouts:', payouts) - const groups = _.groupBy(payouts, (payout) => payout.userId) + const groups = _.groupBy( + [...payouts, ...loanPayouts], + (payout) => payout.userId + ) const userPayouts = _.mapValues(groups, (group) => _.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( ([userId, payout]) => payUser(userId, payout) ) @@ -116,7 +130,7 @@ export const resolveMarket = functions await sendResolutionEmails( openBets, - userPayouts, + userPayoutsWithoutLoans, creator, contract, outcome, diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index dcc29465..e36962a2 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -1,15 +1,20 @@ import clsx from 'clsx' +import _ from 'lodash' import { useUser } from '../hooks/use-user' import { formatMoney } from '../../common/util/format' -import { AddFundsButton } from './add-funds-button' import { Col } from './layout/col' 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: { amount: number | undefined onChange: (newAmount: number | undefined) => void error: string | undefined setError: (error: string | undefined) => void + contractId: string | undefined minimumAmount?: number disabled?: boolean className?: string @@ -22,6 +27,7 @@ export function AmountInput(props: { onChange, error, setError, + contractId, disabled, className, inputClassName, @@ -31,10 +37,24 @@ export function AmountInput(props: { const user = useUser() + const userBets = useUserContractBets(user?.id, contractId) ?? [] + const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale) + const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0) + + const loanAmount = Math.min( + amount ?? 0, + MAX_LOAN_PER_CONTRACT - prevLoanAmount + ) + const onAmountChange = (str: string) => { + if (str.includes('-')) { + onChange(undefined) + return + } const amount = parseInt(str.replace(/[^\d]/, '')) if (str && isNaN(amount)) return + if (amount >= 10 ** 9) return onChange(str ? amount : undefined) @@ -47,7 +67,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 ( @@ -68,19 +89,34 @@ export function AmountInput(props: { onChange={(e) => onAmountChange(e.target.value)} /> + + + {error && ( -
+
{error}
)} {user && ( - -
- Remaining balance -
- -
{formatMoney(Math.floor(remainingBalance))}
- {user.balance !== 1000 && } + + {contractId && ( + + + Amount loaned{' '} + + + {formatMoney(loanAmount)}{' '} + + )} + + Remaining balance{' '} + + {formatMoney(Math.floor(remainingBalance))} + )} diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 33a0593f..26939b35 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -30,8 +30,9 @@ export function AnswerBetPanel(props: { answer: Answer contract: Contract closePanel: () => void + className?: string }) { - const { answer, contract, closePanel } = props + const { answer, contract, closePanel, className } = props const { id: answerId } = answer const user = useUser() @@ -97,7 +98,7 @@ export function AnswerBetPanel(props: { const currentReturnPercent = (currentReturn * 100).toFixed() + '%' return ( - +
Buy this answer
@@ -114,40 +115,44 @@ export function AnswerBetPanel(props: { setError={setError} disabled={isSubmitting} inputRef={inputRef} + contractId={contract.id} /> + + +
Probability
+ +
{formatPercent(initialProb)}
+
+
{formatPercent(resultProb)}
+
+
- - -
Implied probability
- -
{formatPercent(initialProb)}
-
-
{formatPercent(resultProb)}
-
- - - - - Payout if chosen - - -
- {formatMoney(currentPayout)} -   (+{currentReturnPercent}) -
+ + +
Payout if chosen
+ +
+ + + {formatMoney(currentPayout)} + + (+{currentReturnPercent}) + +
+ {user ? (
From 1dd078562a48ee4be343bd4c8028010d968c83cb Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 1 Mar 2022 21:07:22 -0800 Subject: [PATCH 07/31] Fix to actually show investment instead of current value. And subtract bet amount from current value. --- web/components/bets-list.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index fff0975e..7c6e5ffe 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -82,7 +82,8 @@ export function BetsList(props: { user: User }) { if (bet.isSold || bet.sale) return 0 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) }) } ) @@ -137,7 +138,7 @@ export function BetsList(props: { user: User }) {
Invested
- {formatMoney(currentBetsValue)}{' '} + {formatMoney(currentInvestment)}{' '}
From 9e9535ce5641fec50ab2907086480e6425c05d68 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 1 Mar 2022 21:23:37 -0800 Subject: [PATCH 08/31] Subtract loans from leaderboard calculations --- common/scoring.ts | 5 +++-- functions/src/update-user-metrics.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/common/scoring.ts b/common/scoring.ts index 6940a019..f7ed1532 100644 --- a/common/scoring.ts +++ b/common/scoring.ts @@ -45,8 +45,9 @@ export function scoreUsersByContract(contract: Contract, bets: Bet[]) { const investments = bets .filter((bet) => !bet.sale) .map((bet) => { - const { userId, amount } = bet - return { userId, payout: -amount } + const { userId, amount, loanAmount } = bet + const payout = -amount - (loanAmount ?? 0) + return { userId, payout } }) const netPayouts = [...resolvePayouts, ...salePayouts, ...investments] diff --git a/functions/src/update-user-metrics.ts b/functions/src/update-user-metrics.ts index f59d4a34..d1d13727 100644 --- a/functions/src/update-user-metrics.ts +++ b/functions/src/update-user-metrics.ts @@ -52,7 +52,8 @@ const computeInvestmentValue = async ( if (!contract || contract.isResolved) 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) }) } From 2a03751d4990902fe6789b51610d7849553520c2 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 2 Mar 2022 13:43:48 -0800 Subject: [PATCH 09/31] Widen bet panel --- web/pages/[username]/[contractSlug].tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 1020816d..67bc5251 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -116,7 +116,7 @@ export default function ContractPage(props: { )} -
+
- + {allowTrade && ( )} From 6abf2355051424a4ddd7b095c9f8ea84d8a3c824 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 2 Mar 2022 13:50:19 -0800 Subject: [PATCH 10/31] Show correct remaning balance for ante input when creating market --- web/components/amount-input.tsx | 15 +++++++-------- web/components/answers/answer-bet-panel.tsx | 2 +- web/components/answers/create-answer-panel.tsx | 2 +- web/components/bet-panel.tsx | 2 +- web/pages/create.tsx | 2 +- web/pages/make-predictions.tsx | 2 +- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index e36962a2..a29811e9 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -14,7 +14,7 @@ export function AmountInput(props: { onChange: (newAmount: number | undefined) => void error: string | undefined setError: (error: string | undefined) => void - contractId: string | undefined + contractIdForLoan: string | undefined minimumAmount?: number disabled?: boolean className?: string @@ -27,7 +27,7 @@ export function AmountInput(props: { onChange, error, setError, - contractId, + contractIdForLoan, disabled, className, inputClassName, @@ -37,14 +37,13 @@ export function AmountInput(props: { 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 prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0) - const loanAmount = Math.min( - amount ?? 0, - MAX_LOAN_PER_CONTRACT - prevLoanAmount - ) + const loanAmount = contractIdForLoan + ? Math.min(amount ?? 0, MAX_LOAN_PER_CONTRACT - prevLoanAmount) + : 0 const onAmountChange = (str: string) => { if (str.includes('-')) { @@ -99,7 +98,7 @@ export function AmountInput(props: { )} {user && ( - {contractId && ( + {contractIdForLoan && ( Amount loaned{' '} diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 26939b35..d45bdc35 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -115,7 +115,7 @@ export function AnswerBetPanel(props: { setError={setError} disabled={isSubmitting} inputRef={inputRef} - contractId={contract.id} + contractIdForLoan={contract.id} /> diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 2dbadd9e..3f0f5dee 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -101,7 +101,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) { setError={setAmountError} minimumAmount={1} disabled={isSubmitting} - contractId={contract.id} + contractIdForLoan={contract.id} /> diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 67f304c5..89594ae3 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -159,7 +159,7 @@ export function BetPanel(props: { setError={setError} disabled={isSubmitting} inputRef={inputRef} - contractId={contract.id} + contractIdForLoan={contract.id} /> diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 76afa8d2..47ce00d4 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -248,7 +248,7 @@ export function NewContract(props: { question: string; tag?: string }) { error={anteError} setError={setAnteError} disabled={isSubmitting} - contractId={undefined} + contractIdForLoan={undefined} />
diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx index 37872c39..eceb692a 100644 --- a/web/pages/make-predictions.tsx +++ b/web/pages/make-predictions.tsx @@ -245,7 +245,7 @@ ${TEST_VALUE} error={anteError} setError={setAnteError} disabled={isSubmitting} - contractId={undefined} + contractIdForLoan={undefined} />
From 405604adef80d6206f38c11e3162bcd5f43d1fd4 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 2 Mar 2022 14:02:44 -0800 Subject: [PATCH 11/31] Fix insufficient balance error to use bet amount net loan --- web/components/amount-input.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index a29811e9..8f20c0ab 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -57,7 +57,12 @@ export function AmountInput(props: { 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') } else if (minimumAmount && amount < minimumAmount) { setError('Minimum amount: ' + formatMoney(minimumAmount)) From cd8b336635887ff2038de3c0f59592252297d020 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 2 Mar 2022 14:09:53 -0800 Subject: [PATCH 12/31] Enforce a max comment length --- web/components/contract-feed.tsx | 3 ++- web/lib/firebase/comments.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index 5a3be62f..bf5885b5 100644 --- a/web/components/contract-feed.tsx +++ b/web/components/contract-feed.tsx @@ -25,7 +25,7 @@ import { import { useUser } from '../hooks/use-user' import { Linkify } from './linkify' 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 { formatMoney } from '../../common/util/format' import { ResolutionOrChance } from './contract-card' @@ -135,6 +135,7 @@ function FeedBet(props: { activityItem: any; feedType: FeedType }) { className="textarea textarea-bordered w-full" placeholder="Add a comment..." rows={3} + maxLength={MAX_COMMENT_LENGTH} onKeyDown={(e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { submitComment() diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index 34e4bcfe..dfb9a01d 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -14,6 +14,8 @@ import { User } from '../../../common/user' import { Comment } from '../../../common/comment' export type { Comment } +export const MAX_COMMENT_LENGTH = 10000 + export async function createComment( contractId: string, betId: string, @@ -27,7 +29,7 @@ export async function createComment( contractId, betId, userId: commenter.id, - text, + text: text.slice(0, MAX_COMMENT_LENGTH), createdTime: Date.now(), userName: commenter.name, userUsername: commenter.username, From fa817c34a9e24c458504d698bf60ccdcf07001ac Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 2 Mar 2022 14:28:23 -0800 Subject: [PATCH 13/31] Fix more places where insufficient balance error was not accounting for loans --- functions/src/place-bet.ts | 10 ++++------ web/components/answers/answer-bet-panel.tsx | 5 ----- web/components/answers/create-answer-panel.tsx | 3 ++- web/components/bet-panel.tsx | 5 ----- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 4adb1779..f473a2e2 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -9,7 +9,6 @@ import { getNewMultiBetInfo, } from '../../common/new-bet' import { Bet } from '../../common/bet' -import { getValues } from './utils' export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -39,9 +38,6 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( return { status: 'error', message: 'User not found' } const user = userSnap.data() as User - if (user.balance < amount) - return { status: 'error', message: 'Insufficient balance' } - const contractDoc = firestore.doc(`contracts/${contractId}`) const contractSnap = await transaction.get(contractDoc) if (!contractSnap.exists) @@ -57,6 +53,10 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( ) 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') { const answerSnap = await transaction.get( contractDoc.collection('answers').doc(outcome) @@ -69,8 +69,6 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( .collection(`contracts/${contractId}/bets`) .doc() - const loanAmount = getLoanAmount(yourBets, amount) - const { newBet, newPool, newTotalShares, newTotalBets, newBalance } = outcomeType === 'BINARY' ? getNewBinaryBetInfo( diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index d45bdc35..bad83813 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -49,11 +49,6 @@ export function AnswerBetPanel(props: { async function submitBet() { if (!user || !betAmount) return - if (user.balance < betAmount) { - setError('Insufficient balance') - return - } - setError(undefined) setIsSubmitting(true) diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 3f0f5dee..28596521 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -36,6 +36,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) { const submitAnswer = async () => { if (canSubmit) { setIsSubmitting(true) + const result = await createAnswer({ contractId: contract.id, text, @@ -48,7 +49,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) { setText('') setBetAmount(10) setAmountError(undefined) - } + } else setAmountError(result.message) } } diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 89594ae3..ec3e7cfc 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -78,11 +78,6 @@ export function BetPanel(props: { async function submitBet() { if (!user || !betAmount) return - if (user.balance < betAmount) { - setError('Insufficient balance') - return - } - setError(undefined) setIsSubmitting(true) From 6285212a1e06412f78829df50d98136f325a39bd Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 2 Mar 2022 15:42:59 -0800 Subject: [PATCH 14/31] Update your trades to show total profit, invested value --- web/components/bets-list.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 7c6e5ffe..367ec0b6 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -127,8 +127,8 @@ export function BetsList(props: { user: User }) { const totalPortfolio = currentBetsValue + user.balance const totalPnl = totalPortfolio - user.totalDeposits - const totalProfit = (totalPnl / user.totalDeposits) * 100 - const investedProfit = + const totalProfitPercent = (totalPnl / user.totalDeposits) * 100 + const investedProfitPercent = ((currentBetsValue - currentInvestment) / currentInvestment) * 100 return ( @@ -136,17 +136,17 @@ export function BetsList(props: { user: User }) { -
Invested
+
Invested value
- {formatMoney(currentInvestment)}{' '} - + {formatMoney(currentBetsValue)}{' '} +
-
Total portfolio
+
Total profit
- {formatMoney(totalPortfolio)}{' '} - + {formatMoney(totalPnl)}{' '} +
From 4d8af33c85f200ede4bc67e16c025a72aa80647c Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 2 Mar 2022 15:48:38 -0800 Subject: [PATCH 15/31] Invested value => Investment value --- web/components/bets-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 367ec0b6..14679a69 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -136,7 +136,7 @@ export function BetsList(props: { user: User }) { -
Invested value
+
Investment value
{formatMoney(currentBetsValue)}{' '} From c30962bf80c95355ce3271237da137e546ceeb08 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 2 Mar 2022 17:52:41 -0800 Subject: [PATCH 16/31] Create bet count and market count charts in our analytics --- web/components/analytics/charts.tsx | 48 +++++++++++++++++ web/lib/firebase/bets.ts | 21 ++++++++ web/lib/firebase/contracts.ts | 29 ++++++++++ web/pages/analytics.tsx | 84 +++++++++++++++++++++++++---- 4 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 web/components/analytics/charts.tsx diff --git a/web/components/analytics/charts.tsx b/web/components/analytics/charts.tsx new file mode 100644 index 00000000..c628c071 --- /dev/null +++ b/web/components/analytics/charts.tsx @@ -0,0 +1,48 @@ +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[] +}) { + const { dailyCounts, startDate } = 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: 'Yes', data: points, color: '#11b981' }] + + return ( +
= 800 ? 400 : 250 }} + > + dayjs(date).format('MMM DD'), + }} + colors={{ datum: 'color' }} + pointSize={10} + pointBorderWidth={1} + pointBorderColor="#fff" + enableSlices="x" + enableGridX={!!width && width >= 800} + enableArea + margin={{ top: 20, right: 28, bottom: 22, left: 40 }} + /> +
+ ) +} diff --git a/web/lib/firebase/bets.ts b/web/lib/firebase/bets.ts index f03b293b..4056e114 100644 --- a/web/lib/firebase/bets.ts +++ b/web/lib/firebase/bets.ts @@ -103,3 +103,24 @@ export function withoutAnteBets(contract: Contract, bets?: Bet[]) { 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(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 +} diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index eb1b65e1..e75a34fb 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -209,3 +209,32 @@ export async function getClosingSoonContracts() { (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(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 +} diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx index 7f905a46..e4ce95fc 100644 --- a/web/pages/analytics.tsx +++ b/web/pages/analytics.tsx @@ -1,17 +1,81 @@ +import dayjs from 'dayjs' +import _ from 'lodash' +import { DailyCountChart } from '../components/analytics/charts' +import { Col } from '../components/layout/col' import { Page } from '../components/page' +import { Title } from '../components/title' +import { getDailyBets } from '../lib/firebase/bets' +import { getDailyContracts } from '../lib/firebase/contracts' -export default function Analytics() { - // Edit dashboard at https://datastudio.google.com/u/0/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3/edit +export async function getStaticProps() { + const numberOfDays = 80 + const today = dayjs(dayjs().format('YYYY-MM-DD')) + const startDate = today.subtract(numberOfDays, 'day') + + const dailyBets = await getDailyBets(startDate.valueOf(), numberOfDays) + const dailyBetCounts = dailyBets.map((bets) => bets.length) + + const dailyContracts = await getDailyContracts( + startDate.valueOf(), + numberOfDays + ) + const dailyContractCounts = dailyContracts.map( + (contracts) => contracts.length + ) + + return { + props: { + startDate: startDate.valueOf(), + dailyBetCounts, + dailyContractCounts, + }, + revalidate: 12 * 60 * 60, // regenerate after half a day + } +} + +export default function Analytics(props: { + startDate: number + dailyBetCounts: number[] + dailyContractCounts: number[] +}) { return ( - + + ) } + +function CustomAnalytics(props: { + startDate: number + dailyBetCounts: number[] + dailyContractCounts: number[] +}) { + const { startDate, dailyBetCounts, dailyContractCounts } = props + return ( + + + <DailyCountChart dailyCounts={dailyBetCounts} startDate={startDate} /> + + <Title text="Markets count" /> + <DailyCountChart + dailyCounts={dailyContractCounts} + startDate={startDate} + /> + </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 + /> + ) +} From 76c4cd6d687358ab605d499c2aef1ccb7847cc32 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 2 Mar 2022 23:51:58 -0800 Subject: [PATCH 17/31] Render timestamps client-side to fix timezone (#58) * Render timestamps client-side to fix timezone * Fix compilation error --- web/components/client-render.tsx | 13 +++++++++++++ web/components/datetime-tooltip.tsx | 17 ++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 web/components/client-render.tsx diff --git a/web/components/client-render.tsx b/web/components/client-render.tsx new file mode 100644 index 00000000..a58c90ff --- /dev/null +++ b/web/components/client-render.tsx @@ -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 +} diff --git a/web/components/datetime-tooltip.tsx b/web/components/datetime-tooltip.tsx index 6b8e5216..69c4521e 100644 --- a/web/components/datetime-tooltip.tsx +++ b/web/components/datetime-tooltip.tsx @@ -2,6 +2,7 @@ import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import timezone from 'dayjs/plugin/timezone' import advanced from 'dayjs/plugin/advancedFormat' +import { ClientRender } from './client-render' dayjs.extend(utc) dayjs.extend(timezone) @@ -19,13 +20,15 @@ export function DateTimeTooltip(props: { return ( <> - <span - className="tooltip hidden cursor-default sm:inline-block" - data-tip={toolTip} - > - {props.children} - </span> - <span className="sm:hidden whitespace-nowrap">{props.children}</span> + <ClientRender> + <span + className="tooltip hidden cursor-default sm:inline-block" + data-tip={toolTip} + > + {props.children} + </span> + </ClientRender> + <span className="whitespace-nowrap sm:hidden">{props.children}</span> </> ) } From c0ace53df2427ef07d3552e24004b6899ec6a898 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 3 Mar 2022 00:56:03 -0800 Subject: [PATCH 18/31] Improve VisD & copy on landing page --- web/components/feed-create.tsx | 40 ++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/web/components/feed-create.tsx b/web/components/feed-create.tsx index 3f9fa686..0dae424f 100644 --- a/web/components/feed-create.tsx +++ b/web/components/feed-create.tsx @@ -7,44 +7,52 @@ import { ContractsGrid } from './contracts-list' import { Contract } from '../../common/contract' import { Col } from './layout/col' import clsx from 'clsx' +import { SparklesIcon } from '@heroicons/react/solid' +import { Row } from './layout/row' export function FeedPromo(props: { hotContracts: Contract[] }) { const { hotContracts } = props 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"> - <div className="mb-2">Create your own</div> - <div className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent"> - prediction markets + <div className="font-semibold sm:mb-2"> + A{' '} + <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> </h1> <Spacer h={6} /> <div className="mb-4 text-gray-500"> - Find prediction markets run by your favorite creators, or make your - own. + Find prediction markets on any topic imaginable. Or create your own! <br /> - Sign up to get M$ 1,000 for free and start trading! + Sign up to get M$ 1,000 and start trading. <br /> </div> <Spacer h={6} /> <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} > - Sign up now + Sign up for free </button>{' '} </Col> - <Spacer h={6} /> - {/* - <TagsList - className="mt-2" - tags={['#politics', '#crypto', '#covid', '#sports', '#meta']} - /> - <Spacer h={6} /> */} + <Spacer h={12} /> + <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 contracts={hotContracts?.slice(0, 10) || []} showHotVolume From d9fb2c0557d0b637da446ba0baca8b2ecb928fa2 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 3 Mar 2022 01:07:21 -0800 Subject: [PATCH 19/31] Tweak padding --- web/pages/home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/home.tsx b/web/pages/home.tsx index d7afdad1..0a884ab4 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -85,7 +85,7 @@ const Home = (props: { <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"> <div className="tabs"> <div From ef77779ceca05505f3a3ade2031cdef64751f112 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 3 Mar 2022 01:09:32 -0800 Subject: [PATCH 20/31] Format Tailwind classes with Prettier Not sure why Husky hooks aren't enforcing this :shrug: --- web/components/answers/answer-bet-panel.tsx | 14 +++++++------- web/components/answers/answer-resolve-panel.tsx | 4 ++-- web/components/answers/answers-panel.tsx | 4 ++-- web/components/answers/create-answer-panel.tsx | 16 ++++++++-------- web/components/bet-panel.tsx | 10 +++++----- web/components/fast-fold-following.tsx | 2 +- web/pages/add-funds.tsx | 4 ++-- web/pages/create.tsx | 4 ++-- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index bad83813..82b0967b 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -94,11 +94,11 @@ export function AnswerBetPanel(props: { return ( <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> <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> </Row> <div className="my-3 text-left text-sm text-gray-500">Amount </div> @@ -112,8 +112,8 @@ export function AnswerBetPanel(props: { inputRef={inputRef} contractIdForLoan={contract.id} /> - <Col className="gap-3 mt-3 w-full"> - <Row className="justify-between items-center text-sm"> + <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> @@ -122,8 +122,8 @@ export function AnswerBetPanel(props: { </Row> </Row> - <Row className="justify-between items-start text-sm gap-2"> - <Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500"> + <Row className="items-start justify-between gap-2 text-sm"> + <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> <div>Payout if chosen</div> <InfoTooltip text={`Current payout for ${formatWithCommas( @@ -133,7 +133,7 @@ export function AnswerBetPanel(props: { )} shares`} /> </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"> {formatMoney(currentPayout)} </span> diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx index b1146e19..c244d72b 100644 --- a/web/components/answers/answer-resolve-panel.tsx +++ b/web/components/answers/answer-resolve-panel.tsx @@ -71,9 +71,9 @@ export function AnswerResolvePanel(props: { : 'btn-disabled' 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> - <Col className="sm:flex-row sm:items-center gap-4"> + <Col className="gap-4 sm:flex-row sm:items-center"> <ChooseCancelSelector className="sm:!flex-row sm:items-center" selected={resolveOption} diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 5fc30595..9f42efb9 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -95,9 +95,9 @@ export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) { ))} {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:{' '} {formatPercent(getOutcomeProbability(contract.totalShares, '0'))} </div> diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 28596521..8e5b88a8 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -73,7 +73,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) { const currentReturnPercent = (currentReturn * 100).toFixed() + '%' 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"> <div className="mb-1">Add your answer</div> <Textarea @@ -87,14 +87,14 @@ export function CreateAnswerPanel(props: { contract: Contract }) { <div /> <Col className={clsx( - 'sm:flex-row sm:items-end gap-4', + 'gap-4 sm:flex-row sm:items-end', text ? 'justify-between' : 'self-end' )} > {text && ( <> - <Col className="gap-2 mt-1"> - <div className="text-gray-500 text-sm">Buy amount</div> + <Col className="mt-1 gap-2"> + <div className="text-sm text-gray-500">Buy amount</div> <AmountInput amount={betAmount} onChange={setBetAmount} @@ -106,7 +106,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) { /> </Col> <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> <Row> <div>{formatPercent(0)}</div> @@ -115,8 +115,8 @@ export function CreateAnswerPanel(props: { contract: Contract }) { </Row> </Row> - <Row className="justify-between text-sm gap-2"> - <Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500"> + <Row className="justify-between gap-2 text-sm"> + <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> <div>Payout if chosen</div> <InfoTooltip text={`Current payout for ${formatWithCommas( @@ -124,7 +124,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) { )} / ${formatWithCommas(shares)} shares`} /> </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"> {formatMoney(currentPayout)} </span> diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index ec3e7cfc..03b3e34a 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -157,8 +157,8 @@ export function BetPanel(props: { contractIdForLoan={contract.id} /> - <Col className="gap-3 mt-3 w-full"> - <Row className="justify-between items-center text-sm"> + <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> @@ -167,8 +167,8 @@ export function BetPanel(props: { </Row> </Row> - <Row className="justify-between items-start text-sm gap-2"> - <Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500"> + <Row className="items-start justify-between gap-2 text-sm"> + <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> <div> Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} /> </div> @@ -182,7 +182,7 @@ export function BetPanel(props: { )} ${betChoice} shares`} /> </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"> {formatMoney(currentPayout)} </span> diff --git a/web/components/fast-fold-following.tsx b/web/components/fast-fold-following.tsx index db0bd105..577719b6 100644 --- a/web/components/fast-fold-following.tsx +++ b/web/components/fast-fold-following.tsx @@ -32,7 +32,7 @@ function FollowFoldButton(props: { className={clsx( 'rounded-full border-2 px-4 py-1 shadow-md', 'cursor-pointer', - followed ? 'bg-gray-300 border-gray-300' : 'bg-white' + followed ? 'border-gray-300 bg-gray-300' : 'bg-white' )} onClick={onClick} > diff --git a/web/pages/add-funds.tsx b/web/pages/add-funds.tsx index 2ecf6317..339be265 100644 --- a/web/pages/add-funds.tsx +++ b/web/pages/add-funds.tsx @@ -19,10 +19,10 @@ export default function AddFundsPage() { <SEO title="Add funds" description="Add funds" url="/add-funds" /> <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" /> <img - className="mb-6 block self-center -scale-x-100" + className="mb-6 block -scale-x-100 self-center" src="/stylized-crane-black.png" width={200} height={200} diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 47ce00d4..5e8830e0 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -137,7 +137,7 @@ export function NewContract(props: { question: string; tag?: string }) { <span className="mb-1">Answer type</span> </label> <Row className="form-control gap-2"> - <label className="cursor-pointer label gap-2"> + <label className="label cursor-pointer gap-2"> <input className="radio" type="radio" @@ -149,7 +149,7 @@ export function NewContract(props: { question: string; tag?: string }) { <span className="label-text">Yes / No</span> </label> - <label className="cursor-pointer label gap-2"> + <label className="label cursor-pointer gap-2"> <input className="radio" type="radio" From b4f8fcf38ed11ab4442d74a2f08c46f960e60c8a Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 3 Mar 2022 02:10:20 -0800 Subject: [PATCH 21/31] Show profit/loss in sale panel --- web/components/bets-list.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 14679a69..8136042f 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -531,6 +531,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) { ) const saleAmount = calculateSaleAmount(contract, bet) + const profit = saleAmount - bet.amount return ( <ConfirmationButton @@ -558,6 +559,8 @@ function SellButton(props: { contract: Contract; bet: Bet }) { )} <div className="mt-2 mb-1 text-sm"> + {profit > 0 ? 'Profit' : 'Loss'}: {formatMoney(profit).replace('-', '')} + <br /> Market probability: {formatPercent(initialProb)} →{' '} {formatPercent(outcomeProb)} </div> From ed9b20b5ad58acee9a704dc8a4c6d1c885643745 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 3 Mar 2022 13:57:59 -0500 Subject: [PATCH 22/31] fast folds: add russia-ukraine --- web/components/fast-fold-following.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/fast-fold-following.tsx b/web/components/fast-fold-following.tsx index 577719b6..7a296464 100644 --- a/web/components/fast-fold-following.tsx +++ b/web/components/fast-fold-following.tsx @@ -88,7 +88,7 @@ export const FastFoldFollowing = (props: { user={user} followedFoldSlugs={followedFoldSlugs} folds={[ - { name: 'Politics', slug: 'politics' }, + { name: 'Russia/Ukraine', slug: 'russia-ukraine' }, { name: 'Crypto', slug: 'crypto' }, { name: 'Sports', slug: 'sports' }, { name: 'Science', slug: 'science' }, From cb9fa0ceb240dab0a28edd20192ef970a6ed306c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 3 Mar 2022 12:59:12 -0800 Subject: [PATCH 23/31] Daily active users & comments --- web/components/analytics/charts.tsx | 11 +++--- web/lib/firebase/comments.ts | 28 ++++++++++++++ web/pages/analytics.tsx | 58 ++++++++++++++++++++++++----- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/web/components/analytics/charts.tsx b/web/components/analytics/charts.tsx index c628c071..3a315d38 100644 --- a/web/components/analytics/charts.tsx +++ b/web/components/analytics/charts.tsx @@ -6,8 +6,9 @@ import { useWindowSize } from '../../hooks/use-window-size' export function DailyCountChart(props: { startDate: number dailyCounts: number[] + small?: boolean }) { - const { dailyCounts, startDate } = props + const { dailyCounts, startDate, small } = props const { width } = useWindowSize() const dates = dailyCounts.map((_, i) => @@ -18,16 +19,16 @@ export function DailyCountChart(props: { x: date, y: betCount, })) - const data = [{ id: 'Yes', data: points, color: '#11b981' }] + const data = [{ id: 'Count', data: points, color: '#11b981' }] return ( <div className="w-full" - style={{ height: !width || width >= 800 ? 400 : 250 }} + style={{ height: !small && (!width || width >= 800) ? 400 : 250 }} > <ResponsiveLine data={data} - yScale={{ type: 'linear' }} + yScale={{ type: 'linear', stacked: false }} xScale={{ type: 'time', }} @@ -35,7 +36,7 @@ export function DailyCountChart(props: { format: (date) => dayjs(date).format('MMM DD'), }} colors={{ datum: 'color' }} - pointSize={10} + pointSize={width && width >= 800 ? 10 : 0} pointBorderWidth={1} pointBorderColor="#fff" enableSlices="x" diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index dfb9a01d..a6b9d9ea 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -7,6 +7,7 @@ import { where, orderBy, } from 'firebase/firestore' +import _ from 'lodash' import { getValues, listenForValues } from './utils' import { db } from './init' @@ -89,3 +90,30 @@ export function listenForRecentComments( ) { 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 +} diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx index e4ce95fc..268c0d51 100644 --- a/web/pages/analytics.tsx +++ b/web/pages/analytics.tsx @@ -2,9 +2,11 @@ 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 { Title } from '../components/title' import { getDailyBets } from '../lib/firebase/bets' +import { getDailyComments } from '../lib/firebase/comments' import { getDailyContracts } from '../lib/firebase/contracts' export async function getStaticProps() { @@ -12,22 +14,34 @@ export async function getStaticProps() { const today = dayjs(dayjs().format('YYYY-MM-DD')) const startDate = today.subtract(numberOfDays, 'day') - const dailyBets = await getDailyBets(startDate.valueOf(), numberOfDays) - const dailyBetCounts = dailyBets.map((bets) => bets.length) + const [dailyBets, dailyContracts, dailyComments] = await Promise.all([ + getDailyBets(startDate.valueOf(), numberOfDays), + getDailyContracts(startDate.valueOf(), numberOfDays), + getDailyComments(startDate.valueOf(), numberOfDays), + ]) - const dailyContracts = await getDailyContracts( - 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 } @@ -35,12 +49,15 @@ export async function getStaticProps() { export default function Analytics(props: { startDate: number + dailyActiveUsers: number[] dailyBetCounts: number[] dailyContractCounts: number[] + dailyCommentCounts: number[] }) { return ( <Page> <CustomAnalytics {...props} /> + <Spacer h={8} /> <FirebaseAnalytics /> </Page> ) @@ -48,19 +65,42 @@ export default function Analytics(props: { function CustomAnalytics(props: { startDate: number + dailyActiveUsers: number[] dailyBetCounts: number[] dailyContractCounts: number[] + dailyCommentCounts: number[] }) { - const { startDate, dailyBetCounts, dailyContractCounts } = props + const { + startDate, + dailyActiveUsers, + dailyBetCounts, + dailyContractCounts, + dailyCommentCounts, + } = props return ( - <Col className="mb-8"> + <Col> + <Title text="Active users" /> + <DailyCountChart dailyCounts={dailyActiveUsers} startDate={startDate} /> + <Title text="Bets count" /> - <DailyCountChart dailyCounts={dailyBetCounts} startDate={startDate} /> + <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> ) From e49b64ee9fb89aa8093552a09b216f9284e941c2 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 4 Mar 2022 10:33:38 -0800 Subject: [PATCH 24/31] Flex shrink 0 on bet panel container --- web/pages/[username]/[contractSlug].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 67bc5251..38b7cd3c 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -143,7 +143,7 @@ export default function ContractPage(props: { <> <div className="md:ml-6" /> - <Col className="md:w-[310px]"> + <Col className="md:w-[310px] flex-shrink-0"> {allowTrade && ( <BetPanel className="hidden lg:flex" contract={contract} /> )} From d7d8fb94c4c7fd426b7181f67d4842a408a088b4 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Fri, 4 Mar 2022 18:06:11 -0600 Subject: [PATCH 25/31] Trim activity feed (#60) * Trim activity feed: show only items from last 24 hours, remove contract description, shorter width. * Preserve feed ordering based on initial fetch of bets * Add see more button, description to explore items --- .../activity-feed.tsx} | 59 +++-- web/components/contract-feed.tsx | 218 +++++++++++++----- web/hooks/use-find-active-contracts.ts | 11 +- web/pages/fold/[...slugs]/index.tsx | 60 ++--- web/pages/home.tsx | 31 +-- 5 files changed, 238 insertions(+), 141 deletions(-) rename web/{pages/activity.tsx => components/activity-feed.tsx} (74%) diff --git a/web/pages/activity.tsx b/web/components/activity-feed.tsx similarity index 74% rename from web/pages/activity.tsx rename to web/components/activity-feed.tsx index bab58328..bfd4cc1c 100644 --- a/web/pages/activity.tsx +++ b/web/components/activity-feed.tsx @@ -1,9 +1,12 @@ import _ from 'lodash' -import { ContractFeed, ContractSummaryFeed } from '../components/contract-feed' -import { Page } from '../components/page' +import { + ContractActivityFeed, + ContractFeed, + ContractSummaryFeed, +} from './contract-feed' import { Contract } from '../lib/firebase/contracts' import { Comment } from '../lib/firebase/comments' -import { Col } from '../components/layout/col' +import { Col } from './layout/col' import { Bet } from '../../common/bet' const MAX_ACTIVE_CONTRACTS = 75 @@ -72,30 +75,44 @@ export function findActiveContracts( export function ActivityFeed(props: { contracts: Contract[] - contractBets: Bet[][] - contractComments: Comment[][] + recentBets: Bet[] + 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="w-full max-w-3xl"> + <Col className="w-full"> <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"> - <ContractFeed - contract={contract} - bets={contractBets[i]} - comments={contractComments[i]} - feedType="activity" - /> + {loadBetAndCommentHistory ? ( + <ContractFeed + contract={contract} + bets={groupedBets[contract.id] ?? []} + comments={groupedComments[contract.id] ?? []} + feedType="activity" + /> + ) : ( + <ContractActivityFeed + contract={contract} + bets={groupedBets[contract.id] ?? []} + comments={groupedComments[contract.id] ?? []} + /> + )} </div> ))} </Col> </Col> </Col> - ) : ( - <></> ) } @@ -116,11 +133,3 @@ export function SummaryActivityFeed(props: { contracts: Contract[] }) { </Col> ) } - -export default function ActivityPage() { - return ( - <Page> - <ActivityFeed contracts={[]} contractBets={[]} contractComments={[]} /> - </Page> - ) -} diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index bf5885b5..766ba94b 100644 --- a/web/components/contract-feed.tsx +++ b/web/components/contract-feed.tsx @@ -290,7 +290,10 @@ function TruncatedComment(props: { } 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} /> {truncated != comment && ( <SiteLink href={moreHref} className="text-indigo-700"> @@ -301,8 +304,11 @@ function TruncatedComment(props: { ) } -function FeedQuestion(props: { contract: Contract }) { - const { contract } = props +function FeedQuestion(props: { + contract: Contract + showDescription?: boolean +}) { + const { contract, showDescription } = props const { creatorName, creatorUsername, question, resolution, outcomeType } = contract const { truePool } = contractMetrics(contract) @@ -337,22 +343,34 @@ function FeedQuestion(props: { contract: Contract }) { {closeMessage} </span> </div> - <Col className="mb-4 items-start justify-between gap-2 sm:flex-row sm:gap-4"> - <SiteLink - href={contractPath(contract)} - className="text-lg text-indigo-700 sm:text-xl" - > - {question} - </SiteLink> + <Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4"> + <Col> + <SiteLink + href={contractPath(contract)} + className="text-lg text-indigo-700 sm:text-xl" + > + {question} + </SiteLink> + {!showDescription && ( + <SiteLink + href={contractPath(contract)} + className="text-sm relative top-4" + > + <div className="text-gray-500 pb-1.5">See more...</div> + </SiteLink> + )} + </Col> {(isBinary || resolution) && ( <ResolutionOrChance className="items-center" contract={contract} /> )} </Col> - <TruncatedComment - comment={contract.description} - moreHref={contractPath(contract)} - shouldTruncate - /> + {showDescription && ( + <TruncatedComment + comment={contract.description} + moreHref={contractPath(contract)} + shouldTruncate + /> + )} </div> </> ) @@ -681,6 +699,7 @@ type ActivityItem = { | 'close' | 'resolve' | 'expand' + | undefined } type FeedType = @@ -691,64 +710,24 @@ type FeedType = // Grouped for a multi-category outcome | 'multi' -export function ContractFeed(props: { +function FeedItems(props: { contract: Contract - bets: Bet[] - comments: Comment[] + items: ActivityItem[] feedType: FeedType + setExpanded: (expanded: boolean) => void outcome?: string // Which multi-category outcome to filter betRowClassName?: string }) { - const { contract, feedType, outcome, betRowClassName } = props - const { id, outcomeType } = contract + const { contract, items, feedType, outcome, setExpanded, betRowClassName } = + props + const { 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 = [ - { 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 ( <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) => ( - <div key={activityItem.id} className="relative pb-8"> + <div key={activityItem.id} className="relative pb-6"> {activityItemIdx !== items.length - 1 ? ( <span className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200" @@ -792,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: { contract: Contract betRowClassName?: string @@ -805,7 +895,7 @@ export function ContractSummaryFeed(props: { <div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}> <div className="relative pb-8"> <div className="relative flex items-start space-x-3"> - <FeedQuestion contract={contract} /> + <FeedQuestion contract={contract} showDescription /> </div> </div> </div> diff --git a/web/hooks/use-find-active-contracts.ts b/web/hooks/use-find-active-contracts.ts index f8aa5627..2a6a3b47 100644 --- a/web/hooks/use-find-active-contracts.ts +++ b/web/hooks/use-find-active-contracts.ts @@ -4,11 +4,11 @@ import { useMemo, useRef } from 'react' import { Fold } from '../../common/fold' import { User } from '../../common/user' 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 { Contract, getActiveContracts } from '../lib/firebase/contracts' import { listAllFolds } from '../lib/firebase/folds' -import { findActiveContracts } from '../pages/activity' +import { findActiveContracts } from '../components/activity-feed' import { useInactiveContracts } from './use-contracts' import { useFollowedFolds } from './use-fold' import { useUserBetContracts } from './use-user-bets' @@ -20,12 +20,9 @@ export const getAllContractInfo = async () => { listAllFolds().catch(() => []), ]) - const [recentBets, recentComments] = await Promise.all([ - getRecentBets(), - getRecentComments(), - ]) + const recentComments = await getRecentComments() - return { contracts, recentBets, recentComments, folds } + return { contracts, recentComments, folds } } export const useFilterYourContracts = ( diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx index 1d77444b..757fe325 100644 --- a/web/pages/fold/[...slugs]/index.tsx +++ b/web/pages/fold/[...slugs]/index.tsx @@ -12,7 +12,10 @@ import { getFoldBySlug, getFoldContracts, } from '../../../lib/firebase/folds' -import { ActivityFeed, findActiveContracts } from '../../activity' +import { + ActivityFeed, + findActiveContracts, +} from '../../../components/activity-feed' import { TagsList } from '../../../components/tags-list' import { Row } from '../../../components/layout/row' import { UserLink } from '../../../components/user-page' @@ -36,6 +39,9 @@ import { SEO } from '../../../components/SEO' import { useTaggedContracts } from '../../../hooks/use-contracts' import { Linkify } from '../../../components/linkify' 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[] } }) { const { slugs } = props.params @@ -48,7 +54,6 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) { const bets = await Promise.all( contracts.map((contract) => listAllBets(contract.id)) ) - const betsByContract = _.fromPairs(contracts.map((c, i) => [c.id, bets[i]])) let activeContracts = findActiveContracts(contracts, [], _.flatten(bets)) const [resolved, unresolved] = _.partition( @@ -57,10 +62,6 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) { ) activeContracts = [...unresolved, ...resolved] - const activeContractBets = activeContracts.map( - (contract) => betsByContract[contract.id] ?? [] - ) - const creatorScores = scoreCreators(contracts, bets) const traderScores = scoreTraders(contracts, bets) const [topCreators, topTraders] = await Promise.all([ @@ -76,8 +77,6 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) { curator, contracts, activeContracts, - activeContractBets, - activeContractComments: activeContracts.map(() => []), traderScores, topTraders, creatorScores, @@ -117,15 +116,8 @@ export default function FoldPage(props: { creatorScores: { [userId: string]: number } topCreators: User[] }) { - const { - curator, - activeContractBets, - activeContractComments, - traderScores, - topTraders, - creatorScores, - topCreators, - } = props + const { curator, traderScores, topTraders, creatorScores, topCreators } = + props const router = useRouter() const { slugs } = router.query as { slugs: string[] } @@ -151,6 +143,9 @@ export default function FoldPage(props: { props.activeContracts.map((contract) => contractsMap[contract.id]) ) + const recentBets = useRecentBets() + const recentComments = useRecentComments() + if (fold === null || !foldSubpages.includes(page) || slugs[2]) { return <Custom404 /> } @@ -233,19 +228,24 @@ export default function FoldPage(props: { /> )} {page === 'activity' ? ( - <> - <ActivityFeed - contracts={activeContracts} - contractBets={activeContractBets} - contractComments={activeContractComments} - /> - {activeContracts.length === 0 && ( - <div className="mx-2 mt-4 text-gray-500 lg:mx-0"> - No activity from matching markets.{' '} - {isCurator && 'Try editing to add more tags!'} - </div> - )} - </> + recentBets && recentComments ? ( + <> + <ActivityFeed + contracts={activeContracts} + recentBets={recentBets ?? []} + recentComments={recentComments ?? []} + loadBetAndCommentHistory + /> + {activeContracts.length === 0 && ( + <div className="mx-2 mt-4 text-gray-500 lg:mx-0"> + No activity from matching markets.{' '} + {isCurator && 'Try editing to add more tags!'} + </div> + )} + </> + ) : ( + <LoadingIndicator className="mt-4" /> + ) ) : ( <SearchableGrid contracts={contracts} diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 0a884ab4..548f1057 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -6,9 +6,8 @@ import _ from 'lodash' import { Contract } from '../lib/firebase/contracts' import { Page } from '../components/page' -import { ActivityFeed, SummaryActivityFeed } from './activity' +import { ActivityFeed, SummaryActivityFeed } from '../components/activity-feed' import { Comment } from '../lib/firebase/comments' -import { Bet } from '../lib/firebase/bets' import FeedCreate from '../components/feed-create' import { Spacer } from '../components/layout/spacer' import { Col } from '../components/layout/col' @@ -23,8 +22,9 @@ import { useFilterYourContracts, useFindActiveContracts, } 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 { useRecentComments } from '../hooks/use-comments' export async function getStaticProps() { const contractInfo = await getAllContractInfo() @@ -38,10 +38,9 @@ export async function getStaticProps() { const Home = (props: { contracts: Contract[] folds: Fold[] - recentBets: Bet[] recentComments: Comment[] }) => { - const { folds, recentComments } = props + const { folds } = props const user = useUser() const contracts = useActiveContracts() ?? props.contracts @@ -51,13 +50,15 @@ const Home = (props: { contracts ) - const recentBets = useGetRecentBets() - const { activeContracts, activeBets, activeComments } = - useFindActiveContracts({ - contracts: yourContracts, - recentBets: recentBets ?? [], - recentComments, - }) + const initialRecentBets = useGetRecentBets() + const recentBets = useRecentBets() ?? initialRecentBets + const recentComments = useRecentComments() ?? props.recentComments + + const { activeContracts } = useFindActiveContracts({ + contracts: yourContracts, + recentBets: initialRecentBets ?? [], + recentComments: props.recentComments, + }) const exploreContracts = useExploreContracts() @@ -71,7 +72,7 @@ const Home = (props: { return ( <Page assertUser="signed-in"> <Col className="items-center"> - <Col className="w-full max-w-3xl"> + <Col className="w-full max-w-[700px]"> <FeedCreate user={user ?? undefined} /> <Spacer h={6} /> @@ -116,8 +117,8 @@ const Home = (props: { (recentBets ? ( <ActivityFeed contracts={activeContracts} - contractBets={activeBets} - contractComments={activeComments} + recentBets={recentBets} + recentComments={recentComments} /> ) : ( <LoadingIndicator className="mt-4" /> From 4ee458d3f5c2afc2be22e6d8dff78e709b559880 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 5 Mar 2022 23:48:35 -0800 Subject: [PATCH 26/31] Align "See more..." right on mobile --- web/components/contract-feed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index 766ba94b..b4cb7bad 100644 --- a/web/components/contract-feed.tsx +++ b/web/components/contract-feed.tsx @@ -354,7 +354,7 @@ function FeedQuestion(props: { {!showDescription && ( <SiteLink href={contractPath(contract)} - className="text-sm relative top-4" + className="self-end sm:self-start text-sm relative top-4" > <div className="text-gray-500 pb-1.5">See more...</div> </SiteLink> From d8681b043cf3eda5dc72640697671e11cbb670ed Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sat, 5 Mar 2022 23:58:54 -0800 Subject: [PATCH 27/31] Only autofocus question input if no question text and not focused. --- web/components/feed-create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/feed-create.tsx b/web/components/feed-create.tsx index 0dae424f..88c3a211 100644 --- a/web/components/feed-create.tsx +++ b/web/components/feed-create.tsx @@ -109,7 +109,7 @@ export default function FeedCreate(props: { question || focused ? 'ring-2 ring-indigo-300' : '', className )} - onClick={() => !focused && inputRef.current?.focus()} + onClick={() => !focused && !question && inputRef.current?.focus()} ref={(elem) => (panelRef.current = elem)} > <div className="relative flex items-start space-x-3"> From a8fd4c80140e62f53184a03ab8bd79e575885a78 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 6 Mar 2022 00:48:08 -0800 Subject: [PATCH 28/31] Better fix the create market focus issues. Expand to 4 lines when you add many chars to a question. --- web/components/feed-create.tsx | 63 +++++++++++++++++----------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/web/components/feed-create.tsx b/web/components/feed-create.tsx index 88c3a211..2a2d291c 100644 --- a/web/components/feed-create.tsx +++ b/web/components/feed-create.tsx @@ -1,5 +1,6 @@ +import { SparklesIcon, XIcon } from '@heroicons/react/solid' import { Avatar } from './avatar' -import { useEffect, useRef, useState } from 'react' +import { useRef, useState } from 'react' import { Spacer } from './layout/spacer' import { NewContract } from '../pages/create' import { firebaseLogin, User } from '../lib/firebase/users' @@ -7,7 +8,6 @@ import { ContractsGrid } from './contracts-list' import { Contract } from '../../common/contract' import { Col } from './layout/col' import clsx from 'clsx' -import { SparklesIcon } from '@heroicons/react/solid' import { Row } from './layout/row' export function FeedPromo(props: { hotContracts: Contract[] }) { @@ -69,7 +69,8 @@ export default function FeedCreate(props: { }) { const { user, tag, className } = props const [question, setQuestion] = useState('') - const [focused, setFocused] = useState(false) + const [isExpanded, setIsExpanded] = useState(false) + const inputRef = useRef<HTMLTextAreaElement | null>() const placeholders = [ 'Will anyone I know get engaged this year?', @@ -86,60 +87,60 @@ export default function FeedCreate(props: { ) 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 ( <div className={clsx( - 'mt-2 w-full rounded bg-white p-4 shadow-md', - question || focused ? 'ring-2 ring-indigo-300' : '', + 'mt-2 w-full rounded bg-white p-4 shadow-md cursor-text', + isExpanded ? 'ring-2 ring-indigo-300' : '', className )} - onClick={() => !focused && !question && inputRef.current?.focus()} - ref={(elem) => (panelRef.current = elem)} + onClick={() => { + !isExpanded && inputRef.current?.focus() + }} > <div className="relative flex items-start space-x-3"> <Avatar username={user?.username} avatarUrl={user?.avatarUrl} noLink /> <div className="min-w-0 flex-1"> - {/* TODO: Show focus, for accessibility */} - <div> + <Row className="justify-between"> <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 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} value={question} + rows={question.length > 68 ? 4 : 2} onClick={(e) => e.stopPropagation()} onChange={(e) => setQuestion(e.target.value.replace('\n', ''))} - onFocus={() => setFocused(true)} + onFocus={() => setIsExpanded(true)} /> </div> </div> {/* 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} /> </div> {/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/} - {!(question || focused) && ( - <div className="flex justify-end"> + {!isExpanded && ( + <div className="flex justify-end sm:-mt-4"> <button className="btn btn-sm" disabled> Create Market </button> From abd9be921f33cad9aa5c5d9cae7f350a1f1c5c16 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 6 Mar 2022 01:03:06 -0800 Subject: [PATCH 29/31] Allow paying users negative amounts on market resolve. --- functions/src/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 0e87538a..f34db1c8 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -68,8 +68,7 @@ const updateUserBalance = ( } export const payUser = (userId: string, payout: number, isDeposit = false) => { - if (!isFinite(payout) || payout <= 0) - throw new Error('Payout is not positive: ' + payout) + if (!isFinite(payout)) throw new Error('Payout is not finite: ' + payout) return updateUserBalance(userId, payout, isDeposit) } From a32bb57167f5e44510fb547df2ec3a42b72245e1 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Mon, 7 Mar 2022 10:40:53 -0800 Subject: [PATCH 30/31] Update about page --- web/pages/about.tsx | 117 +++++++++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/web/pages/about.tsx b/web/pages/about.tsx index 85e3eaee..3f0fcead 100644 --- a/web/pages/about.tsx +++ b/web/pages/about.tsx @@ -118,6 +118,13 @@ function Contents() { </p> </li> </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-"> Can prediction markets work without real money? </h3> @@ -148,6 +155,40 @@ function Contents() { </a> . </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> <p> The creator of the prediction market decides the outcome and earns{' '} @@ -166,29 +207,9 @@ function Contents() { or even personal. (E.g. "Will I enjoy participating in the Metaverse in 2023?") </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. - </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-"> + {/* <h3 id="how-is-this-different-from-metaculus-or-hypermind-"> How is this different from Metaculus or Hypermind? - </h3> + </h3> */} {/* <p> 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 @@ -199,28 +220,13 @@ function Contents() { carefully and can't rig the outcome by creating multiple accounts. The result is more accurate predictions. </p> */} - <p> + {/* <p> Manifold Markets is focused on accessibility and allowing anyone to 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 judgment on the outcome, it leads to a qualitative shift in the number, variety, and usefulness of prediction markets. - </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> + </p> */} <h3 id="type-of-market-maker">What kind of betting system do you use?</h3> <p> @@ -249,6 +255,20 @@ function Contents() { to find out more! </p> + <h3 id="private-markets">Can I create private markets?</h3> + <p> + Soon! We're running a pilot version of Manifold for Teams - private + Manifold instances where you can discuss internal topics and predict on + outcomes for your organization. + </p> + <p> + If this sounds like something you’d want,{' '} + <a href="https://docs.google.com/forms/d/e/1FAIpQLSfM_rxRHemCjKE6KPiYXGyP2nBSInZNKn_wc7yS1-rvlLAVnA/viewform?usp=sf_link"> + join the waitlist here + </a> + ! + </p> + <h3 id="who-are-we-">Who are we?</h3> <p>Manifold Markets is currently a team of three:</p> <ul> @@ -296,19 +316,24 @@ function Contents() { <h2 id="further-reading">Further Reading</h2> <ul> + <li> + <a href="https://outsidetheasylum.blog/manifold-markets-faq/"> + An in-depth, unofficial FAQ by Isaac King + </a> + </li> <li> <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> </li> <li> <a href="https://sideways-view.com/2019/10/27/prediction-markets-for-internet-points/"> - Paul Christiano: 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 + Paul Christiano on prediction markets for internet points </a> </li> </ul> From ff92338873ff63e072fbec2146eb925dcac2085b Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 7 Mar 2022 13:14:56 -0800 Subject: [PATCH 31/31] Fix multi payout calculation! --- common/payouts.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/payouts.ts b/common/payouts.ts index 446b75d7..ab7f00af 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -138,8 +138,7 @@ export const getPayoutsMultiOutcome = ( const prob = resolutions[outcome] / probTotal const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal const profit = winnings - amount - - const payout = amount + (1 - FEES) * Math.max(0, profit) + const payout = deductFees(amount, winnings) return { userId, profit, payout } })