From 2a03751d4990902fe6789b51610d7849553520c2 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 2 Mar 2022 13:43:48 -0800 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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) }