From aebb4c54832b100ec16666ccffba74271f0bc537 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 19 Mar 2022 12:30:23 -0500 Subject: [PATCH 01/45] formatPercent: only show decimal to at most 2 places --- common/util/format.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/common/util/format.ts b/common/util/format.ts index 7f352cf2..611e5da4 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -18,20 +18,19 @@ export function formatWithCommas(amount: number) { return formatter.format(amount).replace('$', '') } -const decimalPlaces = (x: number) => Math.ceil(-Math.log10(x)) - 2 +export const decimalPlaces = (x: number) => Math.ceil(-Math.log10(x)) - 2 export function formatPercent(decimalPercent: number) { - const displayedFigs = + const decimalFigs = (decimalPercent >= 0.02 && decimalPercent <= 0.98) || decimalPercent <= 0 || decimalPercent >= 1 ? 0 - : Math.max( - decimalPlaces(decimalPercent), - decimalPlaces(1 - decimalPercent) - ) + : decimalPercent >= 0.01 && decimalPercent <= 0.99 + ? 1 + : 2 - return (decimalPercent * 100).toFixed(displayedFigs) + '%' + return (decimalPercent * 100).toFixed(decimalFigs) + '%' } export function toCamelCase(words: string) { From 075804bb70761c2016bca2e62902c6161c76f7a4 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 19 Mar 2022 12:32:22 -0500 Subject: [PATCH 02/45] Revert "formatPercent: only show decimal to at most 2 places" This reverts commit aebb4c54832b100ec16666ccffba74271f0bc537. --- common/util/format.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/common/util/format.ts b/common/util/format.ts index 611e5da4..7f352cf2 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -18,19 +18,20 @@ export function formatWithCommas(amount: number) { return formatter.format(amount).replace('$', '') } -export const decimalPlaces = (x: number) => Math.ceil(-Math.log10(x)) - 2 +const decimalPlaces = (x: number) => Math.ceil(-Math.log10(x)) - 2 export function formatPercent(decimalPercent: number) { - const decimalFigs = + const displayedFigs = (decimalPercent >= 0.02 && decimalPercent <= 0.98) || decimalPercent <= 0 || decimalPercent >= 1 ? 0 - : decimalPercent >= 0.01 && decimalPercent <= 0.99 - ? 1 - : 2 + : Math.max( + decimalPlaces(decimalPercent), + decimalPlaces(1 - decimalPercent) + ) - return (decimalPercent * 100).toFixed(decimalFigs) + '%' + return (decimalPercent * 100).toFixed(displayedFigs) + '%' } export function toCamelCase(words: string) { From 7585a1a649604589fa0a657ae17ebbd3f9c56c0e Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sat, 19 Mar 2022 22:37:57 -0500 Subject: [PATCH 03/45] Include your bet on contracts in default feed --- web/hooks/use-find-active-contracts.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/hooks/use-find-active-contracts.ts b/web/hooks/use-find-active-contracts.ts index b177f93e..ba80a899 100644 --- a/web/hooks/use-find-active-contracts.ts +++ b/web/hooks/use-find-active-contracts.ts @@ -74,7 +74,10 @@ export const useFilterYourContracts = ( if (yourBetContracts && followedFoldIds) { // Show default contracts if no folds are followed. if (followedFoldIds.length === 0) - yourContracts = contracts.filter(includedWithDefaultFeed) + yourContracts = contracts.filter( + (contract) => + includedWithDefaultFeed(contract) || yourBetContracts.has(contract.id) + ) else yourContracts = contracts.filter( (contract) => From 164f5fba06fc4460894f829c28154e3bcc147099 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 20 Mar 2022 12:45:17 -0500 Subject: [PATCH 04/45] Simple market embed page --- web/pages/embed/[username]/[contractSlug].tsx | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 web/pages/embed/[username]/[contractSlug].tsx diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx new file mode 100644 index 00000000..5a4e2d38 --- /dev/null +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -0,0 +1,114 @@ +import { Answer } from '../../../../common/answer' +import { Bet } from '../../../../common/bet' +import { Comment } from '../../../../common/comment' +import { Contract } from '../../../../common/contract' +import { Fold } from '../../../../common/fold' +import { AnswersPanel } from '../../../components/answers/answers-panel' +import { ContractOverview } from '../../../components/contract-overview' +import { Col } from '../../../components/layout/col' +import { Spacer } from '../../../components/layout/spacer' +import { useContractWithPreload } from '../../../hooks/use-contract' +import { useFoldsWithTags } from '../../../hooks/use-fold' +import { fromPropz, usePropz } from '../../../hooks/use-propz' +import { useUser } from '../../../hooks/use-user' +import { listAllAnswers } from '../../../lib/firebase/answers' +import { listAllBets } from '../../../lib/firebase/bets' +import { listAllComments } from '../../../lib/firebase/comments' +import { getContractFromSlug } from '../../../lib/firebase/contracts' +import { getFoldsByTags } from '../../../lib/firebase/folds' +import Custom404 from '../../404' + +export const getStaticProps = fromPropz(getStaticPropz) +export async function getStaticPropz(props: { + params: { username: string; contractSlug: string } +}) { + const { username, contractSlug } = props.params + const contract = (await getContractFromSlug(contractSlug)) || null + const contractId = contract?.id + + const foldsPromise = getFoldsByTags(contract?.tags ?? []) + + const [bets, comments, answers] = await Promise.all([ + contractId ? listAllBets(contractId) : [], + contractId ? listAllComments(contractId) : [], + contractId && contract.outcomeType === 'FREE_RESPONSE' + ? listAllAnswers(contractId) + : [], + ]) + + const folds = await foldsPromise + + return { + props: { + contract, + username, + slug: contractSlug, + bets, + comments, + answers, + folds, + }, + + revalidate: 60, // regenerate after a minute + } +} + +export async function getStaticPaths() { + return { paths: [], fallback: 'blocking' } +} + +export default function ContractPage(props: { + contract: Contract | null + username: string + bets: Bet[] + comments: Comment[] + answers: Answer[] + slug: string + folds: Fold[] +}) { + props = usePropz(props, getStaticPropz) ?? { + contract: null, + username: '', + comments: [], + answers: [], + bets: [], + slug: '', + folds: [], + } + const user = useUser() + + const contract = useContractWithPreload(props.slug, props.contract) + const { bets, comments } = props + + // Sort for now to see if bug is fixed. + comments.sort((c1, c2) => c1.createdTime - c2.createdTime) + bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime) + + const folds = (useFoldsWithTags(contract?.tags) ?? props.folds).filter( + (fold) => fold.followCount > 1 || user?.id === fold.curatorId + ) + + if (!contract) { + return + } + + return ( + + + {contract.outcomeType === 'FREE_RESPONSE' && ( + <> + + + +
+ + )} + + + ) +} From 48f5c28d75a0448840f67446e0c7de54d086829f Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 20 Mar 2022 16:05:16 -0500 Subject: [PATCH 05/45] Simplified contract embed --- web/components/contract-card.tsx | 7 +- web/components/contract-overview.tsx | 2 +- web/pages/embed/[username]/[contractSlug].tsx | 105 +++++++++--------- 3 files changed, 56 insertions(+), 58 deletions(-) diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index 78093d6c..a33b65cc 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -172,8 +172,9 @@ function AbbrContractDetails(props: { export function ContractDetails(props: { contract: Contract isCreator?: boolean + hideTweetBtn?: boolean }) { - const { contract, isCreator } = props + const { contract, isCreator, hideTweetBtn } = props const { closeTime, creatorName, creatorUsername } = contract const { liquidityLabel, createdDate, resolvedDate } = contractMetrics(contract) @@ -233,7 +234,9 @@ export function ContractDetails(props: {
{liquidityLabel}
- + {!hideTweetBtn && ( + + )} ) diff --git a/web/components/contract-overview.tsx b/web/components/contract-overview.tsx index bf2be78d..dc314712 100644 --- a/web/components/contract-overview.tsx +++ b/web/components/contract-overview.tsx @@ -62,7 +62,7 @@ export const ContractOverview = (props: { {(isBinary || resolution) && ( - + c1.createdTime - c2.createdTime) bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime) - const folds = (useFoldsWithTags(contract?.tags) ?? props.folds).filter( - (fold) => fold.followCount > 1 || user?.id === fold.curatorId - ) - if (!contract) { return } + return +} + +function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { + const { contract, bets } = props + const { question, resolution, outcomeType } = contract + + const isBinary = outcomeType === 'BINARY' + return ( - - - {contract.outcomeType === 'FREE_RESPONSE' && ( - <> - - - -
- + +
+ +
+ + + + + + + {(isBinary || resolution) && } + + + + +
+ {isBinary ? ( + + ) : ( + } + bets={bets} + /> )} - +
) } From 087a2a1f8f06a92fe1ddb99bb0232be34982b3dc Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 20 Mar 2022 16:23:25 -0500 Subject: [PATCH 06/45] Monthly active users --- web/components/analytics/charts.tsx | 2 +- web/pages/analytics.tsx | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/web/components/analytics/charts.tsx b/web/components/analytics/charts.tsx index 3a315d38..1f627475 100644 --- a/web/components/analytics/charts.tsx +++ b/web/components/analytics/charts.tsx @@ -36,7 +36,7 @@ export function DailyCountChart(props: { format: (date) => dayjs(date).format('MMM DD'), }} colors={{ datum: 'color' }} - pointSize={width && width >= 800 ? 10 : 0} + pointSize={0} pointBorderWidth={1} pointBorderColor="#fff" enableSlices="x" diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx index c2e0ac99..0afd5a3e 100644 --- a/web/pages/analytics.tsx +++ b/web/pages/analytics.tsx @@ -13,7 +13,7 @@ import { getDailyContracts } from '../lib/firebase/contracts' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz() { - const numberOfDays = 80 + const numberOfDays = 90 const today = dayjs(dayjs().format('YYYY-MM-DD')) const startDate = today.subtract(numberOfDays, 'day') @@ -29,15 +29,26 @@ export async function getStaticPropz() { ) const dailyCommentCounts = dailyComments.map((comments) => comments.length) - const dailyActiveUsers = _.zip(dailyContracts, dailyBets, dailyComments).map( + const dailyUserIds = _.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 _.uniq([...creatorIds, ...betUserIds, ...commentUserIds]) } ) + const dailyActiveUsers = dailyUserIds.map((userIds) => userIds.length) + + const monthlyActiveUsers = dailyUserIds.map((_, i) => { + const start = Math.max(0, i - 30) + const end = i + const uniques = new Set() + for (let j = start; j <= end; j++) + dailyUserIds[j].forEach((userId) => uniques.add(userId)) + return uniques.size + }) + return { props: { startDate: startDate.valueOf(), @@ -45,6 +56,7 @@ export async function getStaticPropz() { dailyBetCounts, dailyContractCounts, dailyCommentCounts, + monthlyActiveUsers, }, revalidate: 12 * 60 * 60, // regenerate after half a day } @@ -56,6 +68,7 @@ export default function Analytics(props: { dailyBetCounts: number[] dailyContractCounts: number[] dailyCommentCounts: number[] + monthlyActiveUsers: number[] }) { props = usePropz(props, getStaticPropz) ?? { startDate: 0, @@ -75,6 +88,7 @@ export default function Analytics(props: { function CustomAnalytics(props: { startDate: number + monthlyActiveUsers: number[] dailyActiveUsers: number[] dailyBetCounts: number[] dailyContractCounts: number[] @@ -82,6 +96,7 @@ function CustomAnalytics(props: { }) { const { startDate, + monthlyActiveUsers, dailyActiveUsers, dailyBetCounts, dailyContractCounts, @@ -89,7 +104,10 @@ function CustomAnalytics(props: { } = props return ( - + <Title text="Monthly Active users" /> + <DailyCountChart dailyCounts={monthlyActiveUsers} startDate={startDate} /> + + <Title text="Daily Active users" /> <DailyCountChart dailyCounts={dailyActiveUsers} startDate={startDate} /> <Title text="Bets count" /> From ee6f91a52f08eadf07446b11aac43484ba8bd7b7 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 20 Mar 2022 17:21:28 -0500 Subject: [PATCH 07/45] Created embed page for analytics --- web/pages/analytics.tsx | 14 +++++----- web/pages/embed/[username]/[contractSlug].tsx | 2 +- web/pages/embed/analytics.tsx | 27 +++++++++++++++++++ 3 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 web/pages/embed/analytics.tsx diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx index 0afd5a3e..b78c2105 100644 --- a/web/pages/analytics.tsx +++ b/web/pages/analytics.tsx @@ -86,7 +86,7 @@ export default function Analytics(props: { ) } -function CustomAnalytics(props: { +export function CustomAnalytics(props: { startDate: number monthlyActiveUsers: number[] dailyActiveUsers: number[] @@ -104,27 +104,27 @@ function CustomAnalytics(props: { } = props return ( <Col> - <Title text="Monthly Active users" /> + <Title text="Monthly Active Users" /> <DailyCountChart dailyCounts={monthlyActiveUsers} startDate={startDate} /> - <Title text="Daily Active users" /> + <Title text="Daily Active Users" /> <DailyCountChart dailyCounts={dailyActiveUsers} startDate={startDate} /> - <Title text="Bets count" /> + <Title text="Trades" /> <DailyCountChart dailyCounts={dailyBetCounts} startDate={startDate} small /> - <Title text="Markets count" /> + <Title text="Markets created" /> <DailyCountChart dailyCounts={dailyContractCounts} startDate={startDate} small /> - <Title text="Comments count" /> + <Title text="Comments" /> <DailyCountChart dailyCounts={dailyCommentCounts} startDate={startDate} @@ -134,7 +134,7 @@ function CustomAnalytics(props: { ) } -function FirebaseAnalytics() { +export function FirebaseAnalytics() { // Edit dashboard at https://datastudio.google.com/u/0/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3/edit return ( <iframe diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index afbd81a5..72ceea93 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -80,7 +80,7 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { return ( <Col className="w-full flex-1 bg-white py-2"> - <div className="px-3 text-xl text-indigo-700"> + <div className="px-3 text-xl md:text-2xl text-indigo-700"> <Linkify text={question} /> </div> diff --git a/web/pages/embed/analytics.tsx b/web/pages/embed/analytics.tsx new file mode 100644 index 00000000..806b6e64 --- /dev/null +++ b/web/pages/embed/analytics.tsx @@ -0,0 +1,27 @@ +import { Col } from '../../components/layout/col' +import { Spacer } from '../../components/layout/spacer' +import { fromPropz } from '../../hooks/use-propz' +import { + CustomAnalytics, + FirebaseAnalytics, + getStaticPropz, +} from '../analytics' + +export const getStaticProps = fromPropz(getStaticPropz) + +export default function AnalyticsEmbed(props: { + startDate: number + dailyActiveUsers: number[] + dailyBetCounts: number[] + dailyContractCounts: number[] + dailyCommentCounts: number[] + monthlyActiveUsers: number[] +}) { + return ( + <Col className="w-full px-2 bg-white"> + <CustomAnalytics {...props} /> + <Spacer h={8} /> + <FirebaseAnalytics /> + </Col> + ) +} From cc15eb2044d0b47018739c6463b52862762cb64d Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 20 Mar 2022 18:12:33 -0500 Subject: [PATCH 08/45] Make embedded market link a clickable link --- web/components/site-link.tsx | 2 +- web/pages/embed/[username]/[contractSlug].tsx | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/web/components/site-link.tsx b/web/components/site-link.tsx index c59e106a..8b19d93b 100644 --- a/web/components/site-link.tsx +++ b/web/components/site-link.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' export const SiteLink = (props: { href: string - children: any + children?: any className?: string }) => { const { href, children, className } = props diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 72ceea93..9a346ef8 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -5,6 +5,7 @@ import { FreeResponse, FullContract, } from '../../../../common/contract' +import { DOMAIN } from '../../../../common/envs/constants' import { AnswersGraph } from '../../../components/answers/answers-graph' import { ResolutionOrChance, @@ -15,10 +16,14 @@ import { Col } from '../../../components/layout/col' import { Row } from '../../../components/layout/row' import { Spacer } from '../../../components/layout/spacer' import { Linkify } from '../../../components/linkify' +import { SiteLink } from '../../../components/site-link' import { useContractWithPreload } from '../../../hooks/use-contract' import { fromPropz, usePropz } from '../../../hooks/use-propz' import { listAllBets } from '../../../lib/firebase/bets' -import { getContractFromSlug } from '../../../lib/firebase/contracts' +import { + contractPath, + getContractFromSlug, +} from '../../../lib/firebase/contracts' import Custom404 from '../../404' export const getStaticProps = fromPropz(getStaticPropz) @@ -78,8 +83,15 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { const isBinary = outcomeType === 'BINARY' + const href = `https://${DOMAIN}${contractPath(contract)}` + return ( - <Col className="w-full flex-1 bg-white py-2"> + <Col className="w-full flex-1 bg-white py-2 relative"> + <SiteLink + className="absolute top-0 left-0 w-full h-full z-20" + href={href} + /> + <div className="px-3 text-xl md:text-2xl text-indigo-700"> <Linkify text={question} /> </div> From 7df69dda4d3ec524ee3be8f4f1ba321bbb345905 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Sun, 20 Mar 2022 18:17:37 -0500 Subject: [PATCH 09/45] Embedded market: Make only top section a link --- web/pages/embed/[username]/[contractSlug].tsx | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 9a346ef8..8b40b2e7 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -86,25 +86,29 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { const href = `https://${DOMAIN}${contractPath(contract)}` return ( - <Col className="w-full flex-1 bg-white py-2 relative"> - <SiteLink - className="absolute top-0 left-0 w-full h-full z-20" - href={href} - /> + <Col className="w-full flex-1 bg-white py-2"> + <Col className="relative"> + <SiteLink + className="absolute top-0 left-0 w-full h-full z-20" + href={href} + /> - <div className="px-3 text-xl md:text-2xl text-indigo-700"> - <Linkify text={question} /> - </div> + <div className="px-3 text-xl md:text-2xl text-indigo-700"> + <Linkify text={question} /> + </div> - <Spacer h={3} /> + <Spacer h={3} /> - <Row className="items-center justify-between gap-4 px-2"> - <ContractDetails contract={contract} isCreator={false} hideTweetBtn /> + <Row className="items-center justify-between gap-4 px-2"> + <ContractDetails contract={contract} isCreator={false} hideTweetBtn /> - {(isBinary || resolution) && <ResolutionOrChance contract={contract} />} - </Row> + {(isBinary || resolution) && ( + <ResolutionOrChance contract={contract} /> + )} + </Row> - <Spacer h={2} /> + <Spacer h={2} /> + </Col> <div className="mx-1"> {isBinary ? ( From 37b8cc9687498ed1c1457b66836394a9de083de6 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sun, 20 Mar 2022 18:07:45 -0700 Subject: [PATCH 10/45] Show a top 5 leaderboard on resolved markets (#66) * Show a top5 leaderboard on resolved markets * Only show profitable traders * Include sales in profits * Copy Leaderboard styling * Also show the top comment and trade * Fix padding for solo bets * Only show both comment & bet if they differ --- web/components/feed/feed-items.tsx | 20 +++- web/components/leaderboard.tsx | 1 + web/hooks/use-users.ts | 16 ++- web/lib/firebase/users.ts | 10 ++ web/pages/[username]/[contractSlug].tsx | 139 +++++++++++++++++++++++- 5 files changed, 178 insertions(+), 8 deletions(-) diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index db526741..24e855a2 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -47,6 +47,7 @@ import { BuyButton } from '../yes-no-selector' import { getDpmOutcomeProbability } from '../../../common/calculate-dpm' import { AnswerBetPanel } from '../answers/answer-bet-panel' import { useSaveSeenContract } from '../../hooks/use-seen-contracts' +import { User } from '../../../common/user' export function FeedItems(props: { contract: Contract @@ -109,7 +110,7 @@ function FeedItem(props: { item: ActivityItem }) { } } -function FeedComment(props: { +export function FeedComment(props: { contract: Contract comment: Comment bet: Bet @@ -171,13 +172,14 @@ function RelativeTimestamp(props: { time: number }) { ) } -function FeedBet(props: { +export function FeedBet(props: { contract: Contract bet: Bet hideOutcome: boolean smallAvatar: boolean + bettor?: User // If set: reveal bettor identity }) { - const { contract, bet, hideOutcome, smallAvatar } = props + const { contract, bet, hideOutcome, smallAvatar, bettor } = props const { id, amount, outcome, createdTime, userId } = bet const user = useUser() const isSelf = user?.id === userId @@ -204,6 +206,13 @@ function FeedBet(props: { avatarUrl={user.avatarUrl} username={user.username} /> + ) : bettor ? ( + <Avatar + className={clsx(smallAvatar && 'ml-1')} + size={smallAvatar ? 'sm' : undefined} + avatarUrl={bettor.avatarUrl} + username={bettor.username} + /> ) : ( <div className="relative px-1"> <div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200"> @@ -212,9 +221,10 @@ function FeedBet(props: { </div> )} </div> - <div className={'min-w-0 flex-1 pb-1.5'}> + <div className={'min-w-0 flex-1 py-1.5'}> <div className="text-sm text-gray-500"> - <span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money} + <span>{isSelf ? 'You' : bettor ? bettor.name : 'A trader'}</span>{' '} + {bought} {money} {!hideOutcome && ( <> {' '} diff --git a/web/components/leaderboard.tsx b/web/components/leaderboard.tsx index 30f4d596..5ae3ddd3 100644 --- a/web/components/leaderboard.tsx +++ b/web/components/leaderboard.tsx @@ -14,6 +14,7 @@ export function Leaderboard(props: { }[] className?: string }) { + // TODO: Ideally, highlight your own entry on the leaderboard const { title, users, columns, className } = props return ( <div className={clsx('w-full px-1', className)}> diff --git a/web/hooks/use-users.ts b/web/hooks/use-users.ts index fbf5feaf..35244d73 100644 --- a/web/hooks/use-users.ts +++ b/web/hooks/use-users.ts @@ -1,6 +1,10 @@ import { useState, useEffect } from 'react' import { PrivateUser, User } from '../../common/user' -import { listenForAllUsers, listenForPrivateUsers } from '../lib/firebase/users' +import { + getUser, + listenForAllUsers, + listenForPrivateUsers, +} from '../lib/firebase/users' export const useUsers = () => { const [users, setUsers] = useState<User[]>([]) @@ -12,6 +16,16 @@ export const useUsers = () => { return users } +export const useUserById = (userId: string) => { + const [user, setUser] = useState<User | undefined>(undefined) + + useEffect(() => { + getUser(userId).then(setUser) + }, [userId]) + + return user +} + export const usePrivateUsers = () => { const [users, setUsers] = useState<PrivateUser[]>([]) diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 599e711a..43253f45 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -126,6 +126,16 @@ export async function uploadData( return await getDownloadURL(uploadRef) } +export async function listUsers(userIds: string[]) { + if (userIds.length > 10) { + throw new Error('Too many users requested at once; Firestore limits to 10') + } + const userCollection = collection(db, 'users') + const q = query(userCollection, where('id', 'in', userIds)) + const docs = await getDocs(q) + return docs.docs.map((doc) => doc.data() as User) +} + export async function listAllUsers() { const userCollection = collection(db, 'users') const q = query(userCollection) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 90929ec9..487942d5 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { useContractWithPreload } from '../../hooks/use-contract' import { ContractOverview } from '../../components/contract-overview' @@ -10,7 +10,7 @@ import { ContractBetsTable, MyBetsSummary } from '../../components/bets-list' import { useBets } from '../../hooks/use-bets' import { Title } from '../../components/title' import { Spacer } from '../../components/layout/spacer' -import { User } from '../../lib/firebase/users' +import { listUsers, User } from '../../lib/firebase/users' import { Contract, getContractFromSlug, @@ -30,6 +30,12 @@ import { listAllAnswers } from '../../lib/firebase/answers' import { Answer } from '../../../common/answer' import { AnswersPanel } from '../../components/answers/answers-panel' import { fromPropz, usePropz } from '../../hooks/use-propz' +import { Leaderboard } from '../../components/leaderboard' +import _ from 'lodash' +import { calculatePayout, resolvedPayout } from '../../../common/calculate' +import { formatMoney } from '../../../common/util/format' +import { FeedBet, FeedComment } from '../../components/feed/feed-items' +import { useUserById } from '../../hooks/use-users' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -147,6 +153,19 @@ export default function ContractPage(props: { )} </ContractOverview> + {contract.isResolved && ( + <> + <div className="grid grid-cols-1 sm:grid-cols-2"> + <ContractLeaderboard contract={contract} bets={bets} /> + <ContractTopTrades + contract={contract} + bets={bets} + comments={comments} + /> + </div> + <Spacer h={12} /> + </> + )} <BetsSection contract={contract} user={user ?? null} bets={bets} /> </div> @@ -195,6 +214,122 @@ function BetsSection(props: { ) } +function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) { + const { contract, bets } = props + const [users, setUsers] = useState<User[]>() + + // Create a map of userIds to total profits (including sales) + const betsByUser = _.groupBy(bets, 'userId') + const userProfits = _.mapValues(betsByUser, (bets) => + _.sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount) + ) + + // Find the 5 users with the most profits + const top5Ids = _.entries(userProfits) + .sort(([i1, p1], [i2, p2]) => p2 - p1) + .filter(([, p]) => p > 0) + .slice(0, 5) + .map(([id]) => id) + + useEffect(() => { + if (top5Ids.length > 0) { + listUsers(top5Ids).then((users) => { + const sortedUsers = _.sortBy(users, (user) => -userProfits[user.id]) + setUsers(sortedUsers) + }) + } + }, []) + + return users && users.length > 0 ? ( + <Leaderboard + title="🏅 Top traders" + users={users || []} + columns={[ + { + header: 'Total profit', + renderCell: (user) => formatMoney(userProfits[user.id] || 0), + }, + ]} + className="mt-12 max-w-sm" + /> + ) : null +} + +function ContractTopTrades(props: { + contract: Contract + bets: Bet[] + comments: Comment[] +}) { + const { contract, bets, comments } = props + const commentsById = _.keyBy(comments, 'id') + const betsById = _.keyBy(bets, 'id') + + // If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit + // Otherwise, we record the profit at resolution time + const profitById: Record<string, number> = {} + for (const bet of bets) { + if (bet.sale) { + const originalBet = betsById[bet.sale.betId] + const profit = bet.sale.amount - originalBet.amount + profitById[bet.id] = profit + profitById[originalBet.id] = profit + } else { + profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount + } + } + + // Now find the betId with the highest profit + const topBetId = _.sortBy(bets, (b) => -profitById[b.id])[0]?.id + const topBettor = useUserById(betsById[topBetId].userId) + + // And also the commentId of the comment with the highest profit + const topCommentId = _.sortBy(comments, (c) => -profitById[c.betId])[0]?.id + + return ( + <div className="mt-12 max-w-sm"> + {topCommentId && ( + <> + <Title text="💬 Proven correct" className="!mt-0" /> + <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> + <FeedComment + contract={contract} + comment={commentsById[topCommentId]} + bet={betsById[topCommentId]} + hideOutcome={false} + truncate={false} + smallAvatar={false} + /> + </div> + <div className="mt-2 text-sm text-gray-500"> + {commentsById[topCommentId].userName} made{' '} + {formatMoney(profitById[topCommentId] || 0)}! + </div> + <Spacer h={16} /> + </> + )} + + {/* If they're the same, only show the comment; otherwise show both */} + {topBettor && topBetId !== topCommentId && ( + <> + <Title text="💸 Smartest money" className="!mt-0" /> + <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> + <FeedBet + contract={contract} + bet={betsById[topBetId]} + hideOutcome={false} + smallAvatar={false} + bettor={topBettor} + /> + </div> + <div className="mt-2 text-sm text-gray-500"> + {topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}! + </div> + </> + )} + </div> + ) +} + const getOpenGraphProps = (contract: Contract) => { const { resolution, question, creatorName, creatorUsername, outcomeType } = contract From 03592f9c3e82d1e02c08c378dbf295f3d3c23609 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Sun, 20 Mar 2022 19:30:04 -0700 Subject: [PATCH 11/45] Add sort by close time --- web/components/bets-list.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index f9c72ce4..cee7e01d 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -37,7 +37,7 @@ import { resolvedPayout, } from '../../common/calculate' -type BetSort = 'newest' | 'profit' | 'settled' | 'value' +type BetSort = 'newest' | 'profit' | 'resolutionTime' | 'value' | 'closeTime' type BetFilter = 'open' | 'closed' | 'resolved' | 'all' export function BetsList(props: { user: User }) { @@ -108,7 +108,8 @@ export function BetsList(props: { user: User }) { value: (c) => contractsCurrentValue[c.id], newest: (c) => Math.max(...contractBets[c.id].map((bet) => bet.createdTime)), - settled: (c) => c.resolutionTime ?? 0, + resolutionTime: (c) => -(c.resolutionTime ?? c.closeTime ?? Infinity), + closeTime: (c) => -(c.closeTime ?? Infinity), } const displayedContracts = _.sortBy(contracts, SORTS[sort]) .reverse() @@ -173,7 +174,8 @@ export function BetsList(props: { user: User }) { <option value="value">By value</option> <option value="profit">By profit</option> <option value="newest">Most recent</option> - <option value="settled">By resolution time</option> + <option value="closeTime">Closing soonest</option> + <option value="resolutionTime">Resolved soonest</option> </select> </Row> </Col> @@ -449,7 +451,7 @@ export function ContractBetsTable(props: { <div className={clsx('overflow-x-auto', className)}> {amountRedeemed > 0 && ( <> - <div className="text-gray-500 text-sm pl-2"> + <div className="pl-2 text-sm text-gray-500"> {amountRedeemed} YES shares and {amountRedeemed} NO shares automatically redeemed for {formatMoney(amountRedeemed)}. </div> @@ -459,7 +461,7 @@ export function ContractBetsTable(props: { {!isResolved && amountLoaned > 0 && ( <> - <div className="text-gray-500 text-sm pl-2"> + <div className="pl-2 text-sm text-gray-500"> You currently have a loan of {formatMoney(amountLoaned)}. </div> <Spacer h={4} /> @@ -505,7 +507,6 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) { shares, isSold, isAnte, - loanAmount, } = bet const { isResolved, closeTime, mechanism } = contract From 0592909248e504eace3b90b85c029af7ca11ff3d Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 21 Mar 2022 01:29:53 -0500 Subject: [PATCH 12/45] Subtract loan amount from investment --- 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 cee7e01d..6eb74e5b 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -91,7 +91,7 @@ export function BetsList(props: { user: User }) { const contractsInvestment = _.mapValues(contractBets, (bets) => { return _.sumBy(bets, (bet) => { if (bet.isSold || bet.sale) return 0 - return bet.amount + return bet.amount - (bet.loanAmount ?? 0) }) }) From 4a617a4c0776d568f609b684a8eda1a1413c0a2a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 21 Mar 2022 15:23:21 -0500 Subject: [PATCH 13/45] Share embed button --- web/components/contract-card.tsx | 12 +++-- web/components/share-embed-button.tsx | 47 +++++++++++++++++++ web/pages/embed/[username]/[contractSlug].tsx | 6 ++- 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 web/components/share-embed-button.tsx diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index a33b65cc..981102cc 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -21,6 +21,7 @@ import { Spacer } from './layout/spacer' import { useState } from 'react' import { TweetButton } from './tweet-button' import { getProbability } from '../../common/calculate' +import { ShareEmbedButton } from './share-embed-button' export function ContractCard(props: { contract: Contract @@ -172,9 +173,9 @@ function AbbrContractDetails(props: { export function ContractDetails(props: { contract: Contract isCreator?: boolean - hideTweetBtn?: boolean + hideShareButtons?: boolean }) { - const { contract, isCreator, hideTweetBtn } = props + const { contract, isCreator, hideShareButtons } = props const { closeTime, creatorName, creatorUsername } = contract const { liquidityLabel, createdDate, resolvedDate } = contractMetrics(contract) @@ -234,8 +235,11 @@ export function ContractDetails(props: { <div className="whitespace-nowrap">{liquidityLabel}</div> </Row> - {!hideTweetBtn && ( - <TweetButton className="self-end" tweetText={tweetText} /> + {!hideShareButtons && ( + <> + <TweetButton className="self-end" tweetText={tweetText} /> + <ShareEmbedButton contract={contract} /> + </> )} </Row> </Col> diff --git a/web/components/share-embed-button.tsx b/web/components/share-embed-button.tsx new file mode 100644 index 00000000..01ccfe00 --- /dev/null +++ b/web/components/share-embed-button.tsx @@ -0,0 +1,47 @@ +import { Fragment } from 'react' +import { CodeIcon } from '@heroicons/react/outline' +import { Menu, Transition } from '@headlessui/react' +import { Contract } from '../../common/contract' +import { contractPath } from '../lib/firebase/contracts' +import { DOMAIN } from '../../common/envs/constants' + +export function ShareEmbedButton(props: { contract: Contract }) { + const { contract } = props + + const copyEmbed = () => { + const title = contract.question + const src = `https://${DOMAIN}/embed${contractPath(contract)}` + + const embedCode = `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>` + + if (navigator.clipboard) navigator.clipboard.writeText(embedCode) + } + + return ( + <Menu as="div" className="relative z-40 flex-shrink-0" onClick={copyEmbed}> + <Menu.Button + className="btn btn-xs btn-outline hover:bg-white hover:text-neutral" + onClick={copyEmbed} + > + <CodeIcon className="w-4 h-4 text-gray-500 mr-1.5" aria-hidden="true" /> + Embed + </Menu.Button> + + <Transition + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + > + <Menu.Items className="absolute left-0 mt-2 w-40 origin-top-center rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> + <Menu.Item> + <div className="px-2 py-1">Embed code copied!</div> + </Menu.Item> + </Menu.Items> + </Transition> + </Menu> + ) +} diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 8b40b2e7..b5816710 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -100,7 +100,11 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { <Spacer h={3} /> <Row className="items-center justify-between gap-4 px-2"> - <ContractDetails contract={contract} isCreator={false} hideTweetBtn /> + <ContractDetails + contract={contract} + isCreator={false} + hideShareButtons + /> {(isBinary || resolution) && ( <ResolutionOrChance contract={contract} /> From 2eeaeff92d724c78f0dd7e4ca76beddc40ceec55 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 21 Mar 2022 15:36:03 -0500 Subject: [PATCH 14/45] Normal case Embed. Tweak gap so contract details fit in one row on more markets --- web/components/contract-card.tsx | 2 +- web/components/share-embed-button.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index 981102cc..8cfd4d3b 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -184,7 +184,7 @@ export function ContractDetails(props: { return ( <Col className="gap-2 text-sm text-gray-500 sm:flex-row sm:flex-wrap"> - <Row className="flex-wrap items-center gap-x-4 gap-y-2"> + <Row className="flex-wrap items-center gap-x-3 gap-y-3"> <Row className="items-center gap-2"> <Avatar username={creatorUsername} diff --git a/web/components/share-embed-button.tsx b/web/components/share-embed-button.tsx index 01ccfe00..d9af7934 100644 --- a/web/components/share-embed-button.tsx +++ b/web/components/share-embed-button.tsx @@ -20,7 +20,7 @@ export function ShareEmbedButton(props: { contract: Contract }) { return ( <Menu as="div" className="relative z-40 flex-shrink-0" onClick={copyEmbed}> <Menu.Button - className="btn btn-xs btn-outline hover:bg-white hover:text-neutral" + className="btn btn-xs btn-outline normal-case hover:bg-white hover:text-neutral" onClick={copyEmbed} > <CodeIcon className="w-4 h-4 text-gray-500 mr-1.5" aria-hidden="true" /> From cf2b54ab8d929bbd7509a8dcabcd657afd125c53 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 21 Mar 2022 16:19:08 -0500 Subject: [PATCH 15/45] Rearrange contract overview (-10 LOC!) --- web/components/contract-card.tsx | 2 +- web/components/contract-overview.tsx | 48 +++++++++++----------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index 8cfd4d3b..f78f1bb4 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -184,7 +184,7 @@ export function ContractDetails(props: { return ( <Col className="gap-2 text-sm text-gray-500 sm:flex-row sm:flex-wrap"> - <Row className="flex-wrap items-center gap-x-3 gap-y-3"> + <Row className="flex-wrap items-center gap-x-4 gap-y-3"> <Row className="items-center gap-2"> <Avatar username={creatorUsername} diff --git a/web/components/contract-overview.tsx b/web/components/contract-overview.tsx index dc314712..5409a81e 100644 --- a/web/components/contract-overview.tsx +++ b/web/components/contract-overview.tsx @@ -34,43 +34,33 @@ export const ContractOverview = (props: { return ( <Col className={clsx('mb-6', className)}> - <Row className="justify-between gap-4 px-2"> - <Col className="gap-4"> + <Col className="gap-4 px-2"> + <Row className="justify-between gap-4"> <div className="text-2xl text-indigo-700 md:text-3xl"> <Linkify text={question} /> </div> - <Row className="items-center justify-between gap-4"> - {(isBinary || resolution) && ( - <ResolutionOrChance - className="md:hidden" - contract={contract} - large - /> - )} - - {isBinary && tradingAllowed(contract) && ( - <BetRow - contract={contract} - className="md:hidden" - labelClassName="hidden" - /> - )} - </Row> - - <ContractDetails contract={contract} isCreator={isCreator} /> - </Col> - - {(isBinary || resolution) && ( - <Col className="hidden md:flex items-end justify-between"> + {(isBinary || resolution) && ( <ResolutionOrChance - className="items-end" + className="hidden md:flex items-end" contract={contract} large /> - </Col> - )} - </Row> + )} + </Row> + + <Row className="md:hidden items-center justify-between gap-4"> + {(isBinary || resolution) && ( + <ResolutionOrChance contract={contract} /> + )} + + {isBinary && tradingAllowed(contract) && ( + <BetRow contract={contract} labelClassName="hidden" /> + )} + </Row> + + <ContractDetails contract={contract} isCreator={isCreator} /> + </Col> <Spacer h={4} /> From 033ff3e15096db4e4aa694f9a698046f88fa6569 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 21 Mar 2022 16:44:11 -0500 Subject: [PATCH 16/45] Implement copy for non-standard browsers. Fix flaky embed copy --- web/components/share-embed-button.tsx | 14 +++++----- web/lib/util/copy.ts | 38 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 web/lib/util/copy.ts diff --git a/web/components/share-embed-button.tsx b/web/components/share-embed-button.tsx index d9af7934..430005d9 100644 --- a/web/components/share-embed-button.tsx +++ b/web/components/share-embed-button.tsx @@ -4,6 +4,7 @@ import { Menu, Transition } from '@headlessui/react' import { Contract } from '../../common/contract' import { contractPath } from '../lib/firebase/contracts' import { DOMAIN } from '../../common/envs/constants' +import { copyToClipboard } from '../lib/util/copy' export function ShareEmbedButton(props: { contract: Contract }) { const { contract } = props @@ -14,15 +15,16 @@ export function ShareEmbedButton(props: { contract: Contract }) { const embedCode = `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>` - if (navigator.clipboard) navigator.clipboard.writeText(embedCode) + copyToClipboard(embedCode) } return ( - <Menu as="div" className="relative z-40 flex-shrink-0" onClick={copyEmbed}> - <Menu.Button - className="btn btn-xs btn-outline normal-case hover:bg-white hover:text-neutral" - onClick={copyEmbed} - > + <Menu + as="div" + className="relative z-40 flex-shrink-0" + onMouseUp={copyEmbed} + > + <Menu.Button className="btn btn-xs btn-outline normal-case hover:bg-white hover:text-neutral"> <CodeIcon className="w-4 h-4 text-gray-500 mr-1.5" aria-hidden="true" /> Embed </Menu.Button> diff --git a/web/lib/util/copy.ts b/web/lib/util/copy.ts new file mode 100644 index 00000000..47dc4dab --- /dev/null +++ b/web/lib/util/copy.ts @@ -0,0 +1,38 @@ +// From: https://stackoverflow.com/a/33928558/1592933 +// Copies a string to the clipboard. Must be called from within an +// event handler such as click. May return false if it failed, but +// this is not always possible. Browser support for Chrome 43+, +// Firefox 42+, Safari 10+, Edge and Internet Explorer 10+. +// Internet Explorer: The clipboard feature may be disabled by +// an administrator. By default a prompt is shown the first +// time the clipboard is used (per session). +export function copyToClipboard(text: string) { + if (navigator.clipboard) { + navigator.clipboard.writeText(text) + } else if ( + (window as any).clipboardData && + (window as any).clipboardData.setData + ) { + console.log('copy 2') + // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. + return (window as any).clipboardData.setData('Text', text) + } else if ( + document.queryCommandSupported && + document.queryCommandSupported('copy') + ) { + console.log('copy 3') + var textarea = document.createElement('textarea') + textarea.textContent = text + textarea.style.position = 'fixed' // Prevent scrolling to bottom of page in Microsoft Edge. + document.body.appendChild(textarea) + textarea.select() + try { + return document.execCommand('copy') // Security exception may be thrown by some browsers. + } catch (ex) { + console.warn('Copy to clipboard failed.', ex) + return prompt('Copy to clipboard: Ctrl+C, Enter', text) + } finally { + document.body.removeChild(textarea) + } + } +} From a9cdeb46a2dc998783ffd90eba88e08d24255016 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Mon, 21 Mar 2022 14:54:09 -0700 Subject: [PATCH 17/45] Clean up button styling --- web/components/share-embed-button.tsx | 13 ++++++++++--- web/components/tweet-button.tsx | 5 +---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/web/components/share-embed-button.tsx b/web/components/share-embed-button.tsx index 430005d9..2c6cba2b 100644 --- a/web/components/share-embed-button.tsx +++ b/web/components/share-embed-button.tsx @@ -24,8 +24,15 @@ export function ShareEmbedButton(props: { contract: Contract }) { className="relative z-40 flex-shrink-0" onMouseUp={copyEmbed} > - <Menu.Button className="btn btn-xs btn-outline normal-case hover:bg-white hover:text-neutral"> - <CodeIcon className="w-4 h-4 text-gray-500 mr-1.5" aria-hidden="true" /> + <Menu.Button + className="btn btn-xs normal-case" + style={{ + backgroundColor: 'white', + border: '2px solid #9ca3af', + color: '#9ca3af', // text-gray-400 + }} + > + <CodeIcon className="mr-1.5 h-4 w-4" aria-hidden="true" /> Embed </Menu.Button> @@ -38,7 +45,7 @@ export function ShareEmbedButton(props: { contract: Contract }) { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - <Menu.Items className="absolute left-0 mt-2 w-40 origin-top-center rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> + <Menu.Items className="origin-top-center absolute left-0 mt-2 w-40 rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> <Menu.Item> <div className="px-2 py-1">Embed code copied!</div> </Menu.Item> diff --git a/web/components/tweet-button.tsx b/web/components/tweet-button.tsx index a9ffa6e3..cbc242dc 100644 --- a/web/components/tweet-button.tsx +++ b/web/components/tweet-button.tsx @@ -5,10 +5,7 @@ export function TweetButton(props: { className?: string; tweetText?: string }) { return ( <a - className={clsx( - 'btn btn-xs flex flex-row flex-nowrap border-none normal-case', - className - )} + className={clsx('btn btn-xs flex-nowrap normal-case', className)} style={{ backgroundColor: 'white', border: '2px solid #1da1f2', From a3067527ee9252bc9a4a1567e6324fa965242af4 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Mon, 21 Mar 2022 15:19:42 -0700 Subject: [PATCH 18/45] Fix crash when there aren't any bets --- web/hooks/use-users.ts | 6 ++++-- web/pages/[username]/[contractSlug].tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/hooks/use-users.ts b/web/hooks/use-users.ts index 35244d73..5d2649b6 100644 --- a/web/hooks/use-users.ts +++ b/web/hooks/use-users.ts @@ -16,11 +16,13 @@ export const useUsers = () => { return users } -export const useUserById = (userId: string) => { +export const useUserById = (userId?: string) => { const [user, setUser] = useState<User | undefined>(undefined) useEffect(() => { - getUser(userId).then(setUser) + if (userId) { + getUser(userId).then(setUser) + } }, [userId]) return user diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 487942d5..7e7e6562 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -280,7 +280,7 @@ function ContractTopTrades(props: { // Now find the betId with the highest profit const topBetId = _.sortBy(bets, (b) => -profitById[b.id])[0]?.id - const topBettor = useUserById(betsById[topBetId].userId) + const topBettor = useUserById(betsById[topBetId]?.userId) // And also the commentId of the comment with the highest profit const topCommentId = _.sortBy(comments, (c) => -profitById[c.betId])[0]?.id From 09da7fcb7cfbbeb18f3b3f30bb4fa66537f27f66 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 21 Mar 2022 17:42:06 -0500 Subject: [PATCH 19/45] Add weekly active users chart. Put daily active first. --- common/util/types.ts | 5 ++++ web/pages/analytics.tsx | 46 +++++++++++++++++++++++++++++------ web/pages/embed/analytics.tsx | 12 +++------ 3 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 common/util/types.ts diff --git a/common/util/types.ts b/common/util/types.ts new file mode 100644 index 00000000..865cb8f3 --- /dev/null +++ b/common/util/types.ts @@ -0,0 +1,5 @@ +export type FirstArgument<T> = T extends (arg1: infer U, ...args: any[]) => any + ? U + : any + +export type Truthy<T> = Exclude<T, undefined | null | false | 0 | ''> diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx index b78c2105..e87fd086 100644 --- a/web/pages/analytics.tsx +++ b/web/pages/analytics.tsx @@ -40,6 +40,15 @@ export async function getStaticPropz() { const dailyActiveUsers = dailyUserIds.map((userIds) => userIds.length) + const weeklyActiveUsers = dailyUserIds.map((_, i) => { + const start = Math.max(0, i - 6) + const end = i + const uniques = new Set<string>() + for (let j = start; j <= end; j++) + dailyUserIds[j].forEach((userId) => uniques.add(userId)) + return uniques.size + }) + const monthlyActiveUsers = dailyUserIds.map((_, i) => { const start = Math.max(0, i - 30) const end = i @@ -53,10 +62,11 @@ export async function getStaticPropz() { props: { startDate: startDate.valueOf(), dailyActiveUsers, + weeklyActiveUsers, + monthlyActiveUsers, dailyBetCounts, dailyContractCounts, dailyCommentCounts, - monthlyActiveUsers, }, revalidate: 12 * 60 * 60, // regenerate after half a day } @@ -65,14 +75,17 @@ export async function getStaticPropz() { export default function Analytics(props: { startDate: number dailyActiveUsers: number[] + weeklyActiveUsers: number[] + monthlyActiveUsers: number[] dailyBetCounts: number[] dailyContractCounts: number[] dailyCommentCounts: number[] - monthlyActiveUsers: number[] }) { props = usePropz(props, getStaticPropz) ?? { startDate: 0, dailyActiveUsers: [], + weeklyActiveUsers: [], + monthlyActiveUsers: [], dailyBetCounts: [], dailyContractCounts: [], dailyCommentCounts: [], @@ -88,27 +101,44 @@ export default function Analytics(props: { export function CustomAnalytics(props: { startDate: number - monthlyActiveUsers: number[] dailyActiveUsers: number[] + weeklyActiveUsers: number[] + monthlyActiveUsers: number[] dailyBetCounts: number[] dailyContractCounts: number[] dailyCommentCounts: number[] }) { const { startDate, - monthlyActiveUsers, dailyActiveUsers, dailyBetCounts, dailyContractCounts, dailyCommentCounts, + weeklyActiveUsers, + monthlyActiveUsers, } = props return ( <Col> - <Title text="Monthly Active Users" /> - <DailyCountChart dailyCounts={monthlyActiveUsers} startDate={startDate} /> - <Title text="Daily Active Users" /> - <DailyCountChart dailyCounts={dailyActiveUsers} startDate={startDate} /> + <DailyCountChart + dailyCounts={dailyActiveUsers} + startDate={startDate} + small + /> + + <Title text="Weekly Active Users" /> + <DailyCountChart + dailyCounts={weeklyActiveUsers} + startDate={startDate} + small + /> + + <Title text="Monthly Active Users" /> + <DailyCountChart + dailyCounts={monthlyActiveUsers} + startDate={startDate} + small + /> <Title text="Trades" /> <DailyCountChart diff --git a/web/pages/embed/analytics.tsx b/web/pages/embed/analytics.tsx index 806b6e64..1441e39e 100644 --- a/web/pages/embed/analytics.tsx +++ b/web/pages/embed/analytics.tsx @@ -1,7 +1,8 @@ +import { FirstArgument } from '../../../common/util/types' import { Col } from '../../components/layout/col' import { Spacer } from '../../components/layout/spacer' import { fromPropz } from '../../hooks/use-propz' -import { +import Analytics, { CustomAnalytics, FirebaseAnalytics, getStaticPropz, @@ -9,14 +10,7 @@ import { export const getStaticProps = fromPropz(getStaticPropz) -export default function AnalyticsEmbed(props: { - startDate: number - dailyActiveUsers: number[] - dailyBetCounts: number[] - dailyContractCounts: number[] - dailyCommentCounts: number[] - monthlyActiveUsers: number[] -}) { +export default function AnalyticsEmbed(props: FirstArgument<typeof Analytics>) { return ( <Col className="w-full px-2 bg-white"> <CustomAnalytics {...props} /> From 28e3adcdffac0df43a2c32ed90ef357d4b5407ac Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 21 Mar 2022 19:34:29 -0500 Subject: [PATCH 20/45] Fix embed button on top of profile menu --- web/components/share-embed-button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/share-embed-button.tsx b/web/components/share-embed-button.tsx index 2c6cba2b..b63dcd60 100644 --- a/web/components/share-embed-button.tsx +++ b/web/components/share-embed-button.tsx @@ -21,7 +21,7 @@ export function ShareEmbedButton(props: { contract: Contract }) { return ( <Menu as="div" - className="relative z-40 flex-shrink-0" + className="relative z-10 flex-shrink-0" onMouseUp={copyEmbed} > <Menu.Button From a37ab956dbde8d5658a7cd668970acd1612c3b9a Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Mon, 21 Mar 2022 19:48:48 -0500 Subject: [PATCH 21/45] Hide answers in graph if M$ 0 is bet on it --- web/components/answers/answers-graph.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/components/answers/answers-graph.tsx b/web/components/answers/answers-graph.tsx index f5d97d04..a602a826 100644 --- a/web/components/answers/answers-graph.tsx +++ b/web/components/answers/answers-graph.tsx @@ -137,12 +137,14 @@ const computeProbsByOutcome = ( bets: Bet[], contract: FullContract<DPM, FreeResponse> ) => { + const { totalBets } = contract + const betsByOutcome = _.groupBy(bets, (bet) => bet.outcome) const outcomes = Object.keys(betsByOutcome).filter((outcome) => { const maxProb = Math.max( ...betsByOutcome[outcome].map((bet) => bet.probAfter) ) - return outcome !== '0' && maxProb > 0.05 + return outcome !== '0' && maxProb > 0.02 && totalBets[outcome] > 0.000000001 }) const trackedOutcomes = _.sortBy( From cc0beb4ca4b2d4f4a0416d2463f1c76c29fac3af Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 22 Mar 2022 00:18:08 -0500 Subject: [PATCH 22/45] DPM: label payout 'Estimated' in bet panel --- web/components/answers/answer-bet-panel.tsx | 6 ++++-- web/components/answers/create-answer-panel.tsx | 6 ++++-- web/components/bet-panel.tsx | 17 +++++++++++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 65758f0b..4e1f61cb 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -134,9 +134,11 @@ export function AnswerBetPanel(props: { </Row> </Row> - <Row className="items-start justify-between gap-2 text-sm"> + <Row className="items-center justify-between gap-2 text-sm"> <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> - <div>Payout if chosen</div> + <div> + Estimated <br /> payout if chosen + </div> <InfoTooltip text={`Current payout for ${formatWithCommas( shares diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 35e04db2..9026a666 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -117,9 +117,11 @@ export function CreateAnswerPanel(props: { </Row> </Row> - <Row className="justify-between gap-2 text-sm"> + <Row className="justify-between gap-4 text-sm items-center"> <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> - <div>Payout if chosen</div> + <div> + Estimated <br /> payout if chosen + </div> <InfoTooltip text={`Current payout for ${formatWithCommas( shares diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 16fb2a4f..fab562b2 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -119,7 +119,7 @@ export function BetPanel(props: { focusAmountInput() } - const tooltip = + const dpmTooltip = contract.mechanism === 'dpm-2' ? `Current payout for ${formatWithCommas(shares)} / ${formatWithCommas( shares + @@ -165,13 +165,22 @@ export function BetPanel(props: { </Row> </Row> - <Row className="items-start justify-between gap-2 text-sm"> + <Row className="items-center 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'} /> + {contract.mechanism === 'dpm-2' ? ( + <> + Estimated + <br /> payout if <OutcomeLabel outcome={betChoice ?? 'YES'} /> + </> + ) : ( + <> + Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} /> + </> + )} </div> - {tooltip && <InfoTooltip text={tooltip} />} + {dpmTooltip && <InfoTooltip text={dpmTooltip} />} </Row> <Row className="flex-wrap items-end justify-end gap-2"> <span className="whitespace-nowrap"> From 1a44124a599119ffacfe0b64892d04a300f54f42 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 22 Mar 2022 00:20:52 -0500 Subject: [PATCH 23/45] Preserve new lines in answer feed item --- web/components/feed/feed-items.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index 24e855a2..89860d76 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -707,7 +707,7 @@ function FeedAnswerGroup(props: { </div> <Row className="align-items justify-between gap-4"> - <span className="text-lg"> + <span className="text-lg whitespace-pre-line"> <Linkify text={text} /> </span> From 6d5a4d6e3f1ea52ff41c07d6cd1160fc8ffd6fb4 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 22 Mar 2022 16:24:26 -0500 Subject: [PATCH 24/45] Week-on-week retention graph --- web/components/analytics/charts.tsx | 48 +++++++++++++++++++++++++++++ web/pages/analytics.tsx | 42 ++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/web/components/analytics/charts.tsx b/web/components/analytics/charts.tsx index 1f627475..47e5b203 100644 --- a/web/components/analytics/charts.tsx +++ b/web/components/analytics/charts.tsx @@ -47,3 +47,51 @@ export function DailyCountChart(props: { </div> ) } + +export function DailyPercentChart(props: { + startDate: number + dailyPercent: number[] + small?: boolean +}) { + const { dailyPercent, startDate, small } = props + const { width } = useWindowSize() + + const dates = dailyPercent.map((_, i) => + dayjs(startDate).add(i, 'day').toDate() + ) + + const points = _.zip(dates, dailyPercent).map(([date, betCount]) => ({ + x: date, + y: betCount, + })) + const data = [{ id: 'Percent', data: points, color: '#11b981' }] + + return ( + <div + className="w-full" + style={{ height: !small && (!width || width >= 800) ? 400 : 250 }} + > + <ResponsiveLine + data={data} + yScale={{ type: 'linear', stacked: false }} + xScale={{ + type: 'time', + }} + axisLeft={{ + format: (value) => `${value}%`, + }} + axisBottom={{ + format: (date) => dayjs(date).format('MMM DD'), + }} + colors={{ datum: 'color' }} + pointSize={0} + pointBorderWidth={1} + pointBorderColor="#fff" + enableSlices="x" + enableGridX={!!width && width >= 800} + enableArea + margin={{ top: 20, right: 28, bottom: 22, left: 40 }} + /> + </div> + ) +} diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx index e87fd086..6c0e428d 100644 --- a/web/pages/analytics.tsx +++ b/web/pages/analytics.tsx @@ -1,7 +1,10 @@ import dayjs from 'dayjs' import _ from 'lodash' import { IS_PRIVATE_MANIFOLD } from '../../common/envs/constants' -import { DailyCountChart } from '../components/analytics/charts' +import { + DailyCountChart, + DailyPercentChart, +} from '../components/analytics/charts' import { Col } from '../components/layout/col' import { Spacer } from '../components/layout/spacer' import { Page } from '../components/page' @@ -58,6 +61,31 @@ export async function getStaticPropz() { return uniques.size }) + const weekOnWeekRetention = dailyUserIds.map((_userId, i) => { + const twoWeeksAgo = { + start: Math.max(0, i - 13), + end: Math.max(0, i - 7), + } + const lastWeek = { + start: Math.max(0, i - 6), + end: i, + } + + const activeTwoWeeksAgo = new Set<string>() + for (let j = twoWeeksAgo.start; j <= twoWeeksAgo.end; j++) { + dailyUserIds[j].forEach((userId) => activeTwoWeeksAgo.add(userId)) + } + const activeLastWeek = new Set<string>() + for (let j = lastWeek.start; j <= lastWeek.end; j++) { + dailyUserIds[j].forEach((userId) => activeLastWeek.add(userId)) + } + const retainedCount = _.sumBy(Array.from(activeTwoWeeksAgo), (userId) => + activeLastWeek.has(userId) ? 1 : 0 + ) + const retainedFrac = retainedCount / activeTwoWeeksAgo.size + return Math.round(retainedFrac * 100 * 100) / 100 + }) + return { props: { startDate: startDate.valueOf(), @@ -67,6 +95,7 @@ export async function getStaticPropz() { dailyBetCounts, dailyContractCounts, dailyCommentCounts, + weekOnWeekRetention, }, revalidate: 12 * 60 * 60, // regenerate after half a day } @@ -80,6 +109,7 @@ export default function Analytics(props: { dailyBetCounts: number[] dailyContractCounts: number[] dailyCommentCounts: number[] + weekOnWeekRetention: number[] }) { props = usePropz(props, getStaticPropz) ?? { startDate: 0, @@ -89,6 +119,7 @@ export default function Analytics(props: { dailyBetCounts: [], dailyContractCounts: [], dailyCommentCounts: [], + weekOnWeekRetention: [], } return ( <Page> @@ -107,6 +138,7 @@ export function CustomAnalytics(props: { dailyBetCounts: number[] dailyContractCounts: number[] dailyCommentCounts: number[] + weekOnWeekRetention: number[] }) { const { startDate, @@ -116,6 +148,7 @@ export function CustomAnalytics(props: { dailyCommentCounts, weeklyActiveUsers, monthlyActiveUsers, + weekOnWeekRetention, } = props return ( <Col> @@ -140,6 +173,13 @@ export function CustomAnalytics(props: { small /> + <Title text="Week-on-week retention" /> + <DailyPercentChart + dailyPercent={weekOnWeekRetention} + startDate={startDate} + small + /> + <Title text="Trades" /> <DailyCountChart dailyCounts={dailyBetCounts} From c40c7af0b013a64bdf27dfc2b7aa964e04e55d80 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 22 Mar 2022 15:53:06 -0700 Subject: [PATCH 25/45] Condense analytics graphs using tabs --- web/pages/analytics.tsx | 200 +++++++++++++++++++++++++++++++--------- 1 file changed, 155 insertions(+), 45 deletions(-) diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx index 6c0e428d..6b2fd038 100644 --- a/web/pages/analytics.tsx +++ b/web/pages/analytics.tsx @@ -1,5 +1,7 @@ +import clsx from 'clsx' import dayjs from 'dayjs' import _ from 'lodash' +import { useState } from 'react' import { IS_PRIVATE_MANIFOLD } from '../../common/envs/constants' import { DailyCountChart, @@ -152,68 +154,176 @@ export function CustomAnalytics(props: { } = props return ( <Col> - <Title text="Daily Active Users" /> - <DailyCountChart - dailyCounts={dailyActiveUsers} - startDate={startDate} - small - /> + <Title text="Active users" /> + <p className="text-gray-500"> + An active user is a user who has traded in, commented on, or created a + market. + </p> + <Spacer h={4} /> - <Title text="Weekly Active Users" /> - <DailyCountChart - dailyCounts={weeklyActiveUsers} - startDate={startDate} - small - /> - - <Title text="Monthly Active Users" /> - <DailyCountChart - dailyCounts={monthlyActiveUsers} - startDate={startDate} - small + <Tabs + defaultIndex={1} + tabs={[ + { + title: 'Daily', + content: ( + <DailyCountChart + dailyCounts={dailyActiveUsers} + startDate={startDate} + small + /> + ), + }, + { + title: 'Weekly', + content: ( + <DailyCountChart + dailyCounts={weeklyActiveUsers} + startDate={startDate} + small + /> + ), + }, + { + title: 'Monthly', + content: ( + <DailyCountChart + dailyCounts={monthlyActiveUsers} + startDate={startDate} + small + /> + ), + }, + ]} /> + <Spacer h={8} /> <Title text="Week-on-week retention" /> + <p className="text-gray-500"> + Out of all active users 2 weeks ago, how many came back last week? + </p> <DailyPercentChart dailyPercent={weekOnWeekRetention} startDate={startDate} small /> + <Spacer h={8} /> - <Title text="Trades" /> - <DailyCountChart - dailyCounts={dailyBetCounts} - startDate={startDate} - small - /> - - <Title text="Markets created" /> - <DailyCountChart - dailyCounts={dailyContractCounts} - startDate={startDate} - small - /> - - <Title text="Comments" /> - <DailyCountChart - dailyCounts={dailyCommentCounts} - startDate={startDate} - small + <Title text="Daily activity" /> + <Tabs + defaultIndex={0} + tabs={[ + { + title: 'Trades', + content: ( + <DailyCountChart + dailyCounts={dailyBetCounts} + startDate={startDate} + small + /> + ), + }, + { + title: 'Markets created', + content: ( + <DailyCountChart + dailyCounts={dailyContractCounts} + startDate={startDate} + small + /> + ), + }, + { + title: 'Comments', + content: ( + <DailyCountChart + dailyCounts={dailyCommentCounts} + startDate={startDate} + small + /> + ), + }, + ]} /> + <Spacer h={8} /> </Col> ) } +type Tab = { + title: string + content: JSX.Element +} + +function Tabs(props: { tabs: Tab[]; defaultIndex: number }) { + const { tabs, defaultIndex } = props + const [activeTab, setActiveTab] = useState(tabs[defaultIndex]) + + return ( + <div> + <div className="sm:hidden"> + <label htmlFor="tabs" className="sr-only"> + Select a tab + </label> + {/* Use an "onChange" listener to redirect the user to the selected tab URL. */} + <select + id="tabs" + name="tabs" + className="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500" + defaultValue={activeTab.title} + > + {tabs.map((tab) => ( + <option key={tab.title}>{tab.title}</option> + ))} + </select> + </div> + <div className="hidden sm:block"> + <nav className="flex space-x-4" aria-label="Tabs"> + {tabs.map((tab) => ( + <a + key={tab.title} + href="#" + className={clsx( + tab.title === activeTab.title + ? 'bg-gray-100 text-gray-700' + : 'text-gray-500 hover:text-gray-700', + 'rounded-md px-3 py-2 text-sm font-medium' + )} + aria-current={tab.title === activeTab.title ? 'page' : undefined} + onClick={(e) => { + e.preventDefault() + setActiveTab(tab) + }} + > + {tab.title} + </a> + ))} + </nav> + </div> + + <div className="mt-4">{activeTab.content}</div> + </div> + ) +} + export 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 - /> + <> + <Title text="Google Analytics" /> + <p className="text-gray-500"> + Less accurate; includes all viewers (not just signed-in users). + </p> + <Spacer h={4} /> + <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 cbc01d81602c1a0b467751b08ce71f9a9a445cee Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 22 Mar 2022 18:26:06 -0500 Subject: [PATCH 26/45] Fix analytics tabs on mobile --- web/pages/analytics.tsx | 61 +++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx index 6b2fd038..3201cd37 100644 --- a/web/pages/analytics.tsx +++ b/web/pages/analytics.tsx @@ -261,45 +261,28 @@ function Tabs(props: { tabs: Tab[]; defaultIndex: number }) { return ( <div> - <div className="sm:hidden"> - <label htmlFor="tabs" className="sr-only"> - Select a tab - </label> - {/* Use an "onChange" listener to redirect the user to the selected tab URL. */} - <select - id="tabs" - name="tabs" - className="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500" - defaultValue={activeTab.title} - > - {tabs.map((tab) => ( - <option key={tab.title}>{tab.title}</option> - ))} - </select> - </div> - <div className="hidden sm:block"> - <nav className="flex space-x-4" aria-label="Tabs"> - {tabs.map((tab) => ( - <a - key={tab.title} - href="#" - className={clsx( - tab.title === activeTab.title - ? 'bg-gray-100 text-gray-700' - : 'text-gray-500 hover:text-gray-700', - 'rounded-md px-3 py-2 text-sm font-medium' - )} - aria-current={tab.title === activeTab.title ? 'page' : undefined} - onClick={(e) => { - e.preventDefault() - setActiveTab(tab) - }} - > - {tab.title} - </a> - ))} - </nav> - </div> + <nav className="flex space-x-4" aria-label="Tabs"> + {tabs.map((tab) => ( + <a + key={tab.title} + href="#" + className={clsx( + tab.title === activeTab.title + ? 'bg-gray-100 text-gray-700' + : 'text-gray-500 hover:text-gray-700', + 'rounded-md px-3 py-2 text-sm font-medium' + )} + aria-current={tab.title === activeTab.title ? 'page' : undefined} + onClick={(e) => { + console.log('clicked') + e.preventDefault() + setActiveTab(tab) + }} + > + {tab.title} + </a> + ))} + </nav> <div className="mt-4">{activeTab.content}</div> </div> From 087e5f89fdb20aae9a1db9b00c118d85221ccb04 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 22 Mar 2022 16:53:23 -0700 Subject: [PATCH 27/45] Revert "formatPercent: always show at least one sig fig" This reverts commit ae0cb4fc8c7e9641216775adbaa9132c1c368580. --- common/util/format.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/common/util/format.ts b/common/util/format.ts index 7f352cf2..05a8f702 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -18,20 +18,10 @@ export function formatWithCommas(amount: number) { return formatter.format(amount).replace('$', '') } -const decimalPlaces = (x: number) => Math.ceil(-Math.log10(x)) - 2 - -export function formatPercent(decimalPercent: number) { - const displayedFigs = - (decimalPercent >= 0.02 && decimalPercent <= 0.98) || - decimalPercent <= 0 || - decimalPercent >= 1 - ? 0 - : Math.max( - decimalPlaces(decimalPercent), - decimalPlaces(1 - decimalPercent) - ) - - return (decimalPercent * 100).toFixed(displayedFigs) + '%' +export function formatPercent(zeroToOne: number) { + // Show 1 decimal place if <2% or >98%, giving more resolution on the tails + const decimalPlaces = zeroToOne < 0.02 || zeroToOne > 0.98 ? 1 : 0 + return (zeroToOne * 100).toFixed(decimalPlaces) + '%' } export function toCamelCase(words: string) { From 9c19966ef93ece853fd57eab3b28bf0fa21a9c16 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 22 Mar 2022 21:05:28 -0500 Subject: [PATCH 28/45] Show fewer graph ticks on mobile so they don't overlap. padding --- web/components/analytics/charts.tsx | 10 ++++++++-- web/pages/analytics.tsx | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/web/components/analytics/charts.tsx b/web/components/analytics/charts.tsx index 47e5b203..4bf8d52b 100644 --- a/web/components/analytics/charts.tsx +++ b/web/components/analytics/charts.tsx @@ -21,9 +21,11 @@ export function DailyCountChart(props: { })) const data = [{ id: 'Count', data: points, color: '#11b981' }] + const bottomAxisTicks = width && width < 600 ? 6 : undefined + return ( <div - className="w-full" + className="w-full overflow-hidden" style={{ height: !small && (!width || width >= 800) ? 400 : 250 }} > <ResponsiveLine @@ -33,6 +35,7 @@ export function DailyCountChart(props: { type: 'time', }} axisBottom={{ + tickValues: bottomAxisTicks, format: (date) => dayjs(date).format('MMM DD'), }} colors={{ datum: 'color' }} @@ -66,9 +69,11 @@ export function DailyPercentChart(props: { })) const data = [{ id: 'Percent', data: points, color: '#11b981' }] + const bottomAxisTicks = width && width < 600 ? 6 : undefined + return ( <div - className="w-full" + className="w-full overflow-hidden" style={{ height: !small && (!width || width >= 800) ? 400 : 250 }} > <ResponsiveLine @@ -81,6 +86,7 @@ export function DailyPercentChart(props: { format: (value) => `${value}%`, }} axisBottom={{ + tickValues: bottomAxisTicks, format: (date) => dayjs(date).format('MMM DD'), }} colors={{ datum: 'color' }} diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx index 3201cd37..f7aac21b 100644 --- a/web/pages/analytics.tsx +++ b/web/pages/analytics.tsx @@ -153,7 +153,7 @@ export function CustomAnalytics(props: { weekOnWeekRetention, } = props return ( - <Col> + <Col className="px-2 sm:px-0"> <Title text="Active users" /> <p className="text-gray-500"> An active user is a user who has traded in, commented on, or created a From 6b61d7209df0e1a665f71bd5554b8aa85ba7bd4b Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 22 Mar 2022 23:49:15 -0500 Subject: [PATCH 29/45] Compute volume for contracts. Show volume instead of liquidity for cpmm. --- common/contract.ts | 1 + common/new-contract.ts | 3 +++ functions/src/create-answer.ts | 3 ++- functions/src/place-bet.ts | 4 +++- functions/src/sell-bet.ts | 3 ++- functions/src/update-contract-metrics.ts | 2 ++ web/lib/firebase/contracts.ts | 5 +---- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/common/contract.ts b/common/contract.ts index 35566c0b..7f5c5fd5 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -29,6 +29,7 @@ export type FullContract< closeEmailsSent?: number + volume?: number volume24Hours: number volume7Days: number diff --git a/common/new-contract.ts b/common/new-contract.ts index 3b3278c2..ffd27e3f 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -34,6 +34,8 @@ export function getNewContract( ? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante) : getFreeAnswerProps(ante) + const volume = outcomeType === 'BINARY' ? 0 : ante + const contract: Contract = removeUndefinedProps({ id, slug, @@ -54,6 +56,7 @@ export function getNewContract( lastUpdatedTime: Date.now(), closeTime, + volume, volume24Hours: 0, volume7Days: 0, diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index 17d085f5..bc075fb9 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -57,7 +57,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( message: 'Requires a free response contract', } - const { closeTime } = contract + const { closeTime, volume } = contract if (closeTime && Date.now() > closeTime) return { status: 'error', message: 'Trading is closed' } @@ -121,6 +121,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( totalShares: newTotalShares, totalBets: newTotalBets, answers: [...(contract.answers ?? []), answer], + volume: (volume ?? 0) + amount, }) if (!isFinite(newBalance)) { diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 1a79287d..f2ce3bc0 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -49,7 +49,8 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract - const { closeTime, outcomeType, mechanism, collectedFees } = contract + const { closeTime, outcomeType, mechanism, collectedFees, volume } = + contract if (closeTime && Date.now() > closeTime) return { status: 'error', message: 'Trading is closed' } @@ -129,6 +130,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( totalBets: newTotalBets, totalLiquidity: newTotalLiquidity, collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}), + volume: (volume ?? 0) + Math.abs(amount), }) ) diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index a5bb2af9..1dd57d2b 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -35,7 +35,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract - const { closeTime, mechanism, collectedFees } = contract + const { closeTime, mechanism, collectedFees, volume } = contract if (closeTime && Date.now() > closeTime) return { status: 'error', message: 'Trading is closed' } @@ -81,6 +81,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( totalShares: newTotalShares, totalBets: newTotalBets, collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}), + volume: (volume ?? 0) + bet.amount, }) ) diff --git a/functions/src/update-contract-metrics.ts b/functions/src/update-contract-metrics.ts index c3801df6..d95dcd80 100644 --- a/functions/src/update-contract-metrics.ts +++ b/functions/src/update-contract-metrics.ts @@ -22,9 +22,11 @@ export const updateContractMetrics = functions.pubsub contracts.map((contract) => async () => { const volume24Hours = await computeVolumeFrom(contract, oneDay) const volume7Days = await computeVolumeFrom(contract, oneDay * 7) + const volume = await computeVolumeFrom(contract, oneDay * 365) const contractRef = firestore.doc(`contracts/${contract.id}`) return contractRef.update({ + volume, volume24Hours, volume7Days, }) diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 70e09deb..22e07239 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -22,7 +22,6 @@ import { getDpmProbability } from '../../../common/calculate-dpm' import { createRNG, shuffle } from '../../../common/util/random' import { getCpmmProbability } from '../../../common/calculate-cpmm' import { formatMoney, formatPercent } from '../../../common/util/format' -import { getCpmmLiquidity } from '../../../common/calculate-cpmm' export type { Contract } export function contractPath(contract: Contract) { @@ -43,9 +42,7 @@ export function contractMetrics(contract: Contract) { const liquidityLabel = contract.mechanism === 'dpm-2' ? `${formatMoney(truePool)} pool` - : `${formatMoney( - contract.totalLiquidity ?? getCpmmLiquidity(pool, contract.p) - )} liquidity` + : `${formatMoney(contract.volume ?? contract.volume7Days)} volume` return { truePool, liquidityLabel, createdDate, resolvedDate } } From 510e4400d30e3ace15cc88d7e7ada478b6fefd2d Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 23 Mar 2022 00:02:47 -0500 Subject: [PATCH 30/45] Rename liquidity label to volume label --- web/components/contract-card.tsx | 14 ++++++-------- web/components/feed/feed-items.tsx | 4 ++-- web/lib/firebase/contracts.ts | 6 +++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index f78f1bb4..f1a1c3b2 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -131,7 +131,7 @@ function AbbrContractDetails(props: { }) { const { contract, showHotVolume, showCloseTime } = props const { volume24Hours, creatorName, creatorUsername, closeTime } = contract - const { liquidityLabel } = contractMetrics(contract) + const { volumeLabel } = contractMetrics(contract) return ( <Col className={clsx('gap-2 text-sm text-gray-500')}> @@ -162,7 +162,7 @@ function AbbrContractDetails(props: { ) : ( <Row className="gap-1"> {/* <DatabaseIcon className="h-5 w-5" /> */} - {liquidityLabel} + {volumeLabel} </Row> )} </Row> @@ -177,8 +177,7 @@ export function ContractDetails(props: { }) { const { contract, isCreator, hideShareButtons } = props const { closeTime, creatorName, creatorUsername } = contract - const { liquidityLabel, createdDate, resolvedDate } = - contractMetrics(contract) + const { volumeLabel, createdDate, resolvedDate } = contractMetrics(contract) const tweetText = getTweetText(contract, !!isCreator) @@ -232,7 +231,7 @@ export function ContractDetails(props: { <Row className="items-center gap-1"> <DatabaseIcon className="h-5 w-5" /> - <div className="whitespace-nowrap">{liquidityLabel}</div> + <div className="whitespace-nowrap">{volumeLabel}</div> </Row> {!hideShareButtons && ( @@ -249,8 +248,7 @@ export function ContractDetails(props: { // String version of the above, to send to the OpenGraph image generator export function contractTextDetails(contract: Contract) { const { closeTime, tags } = contract - const { createdDate, resolvedDate, liquidityLabel } = - contractMetrics(contract) + const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract) const hashtags = tags.map((tag) => `#${tag}`) @@ -261,7 +259,7 @@ export function contractTextDetails(contract: Contract) { closeTime ).format('MMM D, h:mma')}` : '') + - ` • ${liquidityLabel}` + + ` • ${volumeLabel}` + (hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '') ) } diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index 89860d76..b85669ba 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -417,7 +417,7 @@ export function FeedQuestion(props: { const { contract, showDescription } = props const { creatorName, creatorUsername, question, resolution, outcomeType } = contract - const { liquidityLabel } = contractMetrics(contract) + const { volumeLabel } = contractMetrics(contract) const isBinary = outcomeType === 'BINARY' const closeMessage = @@ -445,7 +445,7 @@ export function FeedQuestion(props: { asked {/* Currently hidden on mobile; ideally we'd fit this in somewhere. */} <span className="float-right hidden text-gray-400 sm:inline"> - {liquidityLabel} + {volumeLabel} {closeMessage} </span> </div> diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 22e07239..a9ea2780 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -39,12 +39,12 @@ export function contractMetrics(contract: Contract) { ? dayjs(resolutionTime).format('MMM D') : undefined - const liquidityLabel = + const volumeLabel = contract.mechanism === 'dpm-2' ? `${formatMoney(truePool)} pool` - : `${formatMoney(contract.volume ?? contract.volume7Days)} volume` + : `${formatMoney(contract.volume)} volume` - return { truePool, liquidityLabel, createdDate, resolvedDate } + return { truePool, volumeLabel, createdDate, resolvedDate } } export function getBinaryProb(contract: FullContract<any, Binary>) { From 69e142e279f11e59a28ab5e7fcbf96935f8f31f7 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 23 Mar 2022 00:09:47 -0500 Subject: [PATCH 31/45] Make volume a non-optional field of contract --- common/contract.ts | 2 +- functions/src/create-answer.ts | 2 +- functions/src/place-bet.ts | 2 +- functions/src/sell-bet.ts | 2 +- functions/src/update-contract-metrics.ts | 2 -- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/common/contract.ts b/common/contract.ts index 7f5c5fd5..77568f49 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -29,7 +29,7 @@ export type FullContract< closeEmailsSent?: number - volume?: number + volume: number volume24Hours: number volume7Days: number diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index bc075fb9..fbde2666 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -121,7 +121,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( totalShares: newTotalShares, totalBets: newTotalBets, answers: [...(contract.answers ?? []), answer], - volume: (volume ?? 0) + amount, + volume: volume + amount, }) if (!isFinite(newBalance)) { diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index f2ce3bc0..68789096 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -130,7 +130,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( totalBets: newTotalBets, totalLiquidity: newTotalLiquidity, collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}), - volume: (volume ?? 0) + Math.abs(amount), + volume: volume + Math.abs(amount), }) ) diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index 1dd57d2b..4b31cfde 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -81,7 +81,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( totalShares: newTotalShares, totalBets: newTotalBets, collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}), - volume: (volume ?? 0) + bet.amount, + volume: volume + bet.amount, }) ) diff --git a/functions/src/update-contract-metrics.ts b/functions/src/update-contract-metrics.ts index d95dcd80..c3801df6 100644 --- a/functions/src/update-contract-metrics.ts +++ b/functions/src/update-contract-metrics.ts @@ -22,11 +22,9 @@ export const updateContractMetrics = functions.pubsub contracts.map((contract) => async () => { const volume24Hours = await computeVolumeFrom(contract, oneDay) const volume7Days = await computeVolumeFrom(contract, oneDay * 7) - const volume = await computeVolumeFrom(contract, oneDay * 365) const contractRef = firestore.doc(`contracts/${contract.id}`) return contractRef.update({ - volume, volume24Hours, volume7Days, }) From 364c6ad8e573035d0ea34e34919a7320e7b0fb76 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 23 Mar 2022 00:23:40 -0500 Subject: [PATCH 32/45] Show volume label for DPM contracts too --- web/lib/firebase/contracts.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index a9ea2780..41f94e65 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -39,10 +39,7 @@ export function contractMetrics(contract: Contract) { ? dayjs(resolutionTime).format('MMM D') : undefined - const volumeLabel = - contract.mechanism === 'dpm-2' - ? `${formatMoney(truePool)} pool` - : `${formatMoney(contract.volume)} volume` + const volumeLabel = `${formatMoney(contract.volume)} volume` return { truePool, volumeLabel, createdDate, resolvedDate } } From a967f7459dde56a2cbccdfafd9e975b066cf99cd Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 23 Mar 2022 00:27:22 -0500 Subject: [PATCH 33/45] Sort most traded by volume instead of pool size --- web/components/contracts-list.tsx | 6 +----- web/lib/firebase/contracts.ts | 6 ++---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx index 0a294e54..0c45da1c 100644 --- a/web/components/contracts-list.tsx +++ b/web/components/contracts-list.tsx @@ -7,7 +7,6 @@ import { contractMetrics, Contract, listContracts, - getBinaryProbPercent, getBinaryProb, } from '../lib/firebase/contracts' import { User } from '../lib/firebase/users' @@ -16,7 +15,6 @@ import { SiteLink } from './site-link' import { ContractCard } from './contract-card' import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params' import { Answer } from '../../common/answer' -import { getProbability } from '../../common/calculate' export function ContractsGrid(props: { contracts: Contract[] @@ -249,9 +247,7 @@ export function SearchableGrid(props: { ({ closeTime }) => closeTime && closeTime > Date.now() !== hideClosed ) } else if (sort === 'most-traded') { - matches.sort( - (a, b) => contractMetrics(b).truePool - contractMetrics(a).truePool - ) + matches.sort((a, b) => b.volume - a.volume) } else if (sort === '24-hour-vol') { // Use lodash for stable sort, so previous sort breaks all ties. matches = _.sortBy(matches, ({ volume7Days }) => -1 * volume7Days) diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 41f94e65..46474c3f 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -29,9 +29,7 @@ export function contractPath(contract: Contract) { } export function contractMetrics(contract: Contract) { - const { pool, createdTime, resolutionTime, isResolved } = contract - - const truePool = _.sum(Object.values(pool)) + const { createdTime, resolutionTime, isResolved } = contract const createdDate = dayjs(createdTime).format('MMM D') @@ -41,7 +39,7 @@ export function contractMetrics(contract: Contract) { const volumeLabel = `${formatMoney(contract.volume)} volume` - return { truePool, volumeLabel, createdDate, resolvedDate } + return { volumeLabel, createdDate, resolvedDate } } export function getBinaryProb(contract: FullContract<any, Binary>) { From 8569a0362b6930d289d38f91ec1ea82a899de68d Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 23 Mar 2022 00:34:04 -0500 Subject: [PATCH 34/45] Fix not being able to go back on markets pages / communities --- web/hooks/use-sort-and-query-params.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index 74de7f4c..642a4e90 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -69,7 +69,9 @@ export function useQueryAndSortParams(options?: { if (router.isReady && !sort && shouldLoadFromStorage) { const localSort = localStorage.getItem(MARKETS_SORT) as Sort if (localSort) { - setSort(localSort) + router.query.s = localSort + // Use replace to not break navigating back. + router.replace(router, undefined, { shallow: true }) } } }) From 7696dd84b54930c1b1e2d5d96a4f4e48a30f606b Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 23 Mar 2022 00:40:05 -0500 Subject: [PATCH 35/45] Fix 'undefined' shares in DPM tooltip --- web/components/bet-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index fab562b2..2aebc241 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -127,7 +127,7 @@ export function BetPanel(props: { (contract.phantomShares ? contract.phantomShares[betChoice ?? 'YES'] : 0) - )} ${betChoice} shares` + )} ${betChoice ?? 'YES'} shares` : undefined return ( From 1374309de3481c498a2beedb72768a10de66f6d8 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 23 Mar 2022 12:06:39 -0700 Subject: [PATCH 36/45] Only show profitable trades in "Smartest Money" --- 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 7e7e6562..1c321abc 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -287,7 +287,7 @@ function ContractTopTrades(props: { return ( <div className="mt-12 max-w-sm"> - {topCommentId && ( + {topCommentId && profitById[topCommentId] > 0 && ( <> <Title text="💬 Proven correct" className="!mt-0" /> <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> @@ -309,7 +309,7 @@ function ContractTopTrades(props: { )} {/* If they're the same, only show the comment; otherwise show both */} - {topBettor && topBetId !== topCommentId && ( + {topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && ( <> <Title text="💸 Smartest money" className="!mt-0" /> <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> From 1a1dc97ec89c3a58b56a5b61a875c343e2f929a8 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 24 Mar 2022 09:28:36 -0700 Subject: [PATCH 37/45] Generate a sitemap with next-sitemap --- web/next-sitemap.js | 43 +++++++++++++++++++++++++++++++++++++++++++ web/package.json | 4 +++- yarn.lock | 18 ++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 web/next-sitemap.js diff --git a/web/next-sitemap.js b/web/next-sitemap.js new file mode 100644 index 00000000..3298f52b --- /dev/null +++ b/web/next-sitemap.js @@ -0,0 +1,43 @@ +const https = require('https') + +/** @type {import('next-sitemap').IConfig} */ + +module.exports = { + siteUrl: process.env.SITE_URL || 'https://manifold.markets', + changefreq: 'hourly', + priority: 0.7, // Set high priority by default + additionalPaths, + exclude: ['/admin'], + generateRobotsTxt: true, + // Other options: https://github.com/iamvishnusankar/next-sitemap#configuration-options +} + +// See https://github.com/iamvishnusankar/next-sitemap#additional-paths-function +async function additionalPaths(config) { + // Fetching data from https://docs.manifold.markets/api + const response = await fetch(`${config.siteUrl}/api/v0/markets`) + + const liteMarkets = await response + // See https://www.sitemaps.org/protocol.html + return liteMarkets.map((liteMarket) => ({ + loc: liteMarket.url, + changefreq: 'hourly', + priority: 0.2, // Individual markets aren't that important + // TODO: Add `lastmod` aka last modified time + })) +} + +// Polyfill for fetch: get the JSON contents of a URL +async function fetch(url) { + return new Promise((resolve, reject) => { + https.get(url, (res) => { + let data = '' + res.on('data', (chunk) => { + data += chunk + }) + res.on('end', () => { + resolve(JSON.parse(data)) + }) + }) + }) +} diff --git a/web/package.json b/web/package.json index 61342e4d..e5225454 100644 --- a/web/package.json +++ b/web/package.json @@ -12,7 +12,8 @@ "start": "next start", "lint": "next lint", "format": "npx prettier --write .", - "prepare": "cd .. && husky install web/.husky" + "prepare": "cd .. && husky install web/.husky", + "postbuild": "next-sitemap" }, "dependencies": { "@headlessui/react": "1.4.2", @@ -44,6 +45,7 @@ "eslint-config-next": "12.0.4", "husky": "7.0.4", "lint-staged": "12.1.3", + "next-sitemap": "^2.5.14", "postcss": "8.3.5", "prettier": "2.5.0", "prettier-plugin-tailwindcss": "^0.1.5", diff --git a/yarn.lock b/yarn.lock index 2afa5dda..dc454850 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,6 +72,11 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@corex/deepmerge@^2.6.148": + version "2.6.148" + resolved "https://registry.yarnpkg.com/@corex/deepmerge/-/deepmerge-2.6.148.tgz#8fa825d53ffd1cbcafce1b6a830eefd3dcc09dd5" + integrity sha512-6QMz0/2h5C3ua51iAnXMPWFbb1QOU1UvSM4bKBw5mzdT+WtLgjbETBBIQZ+Sh9WvEcGwlAt/DEdRpIC3XlDBMA== + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -4322,6 +4327,11 @@ minimist@^1.1.1, minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + mri@^1.1.5: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -4373,6 +4383,14 @@ netmask@^1.0.6: resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU= +next-sitemap@^2.5.14: + version "2.5.14" + resolved "https://registry.yarnpkg.com/next-sitemap/-/next-sitemap-2.5.14.tgz#f196c90d4aef8444c6eb7266875bf2179a515bb7" + integrity sha512-aJmxGmoE23NClCi1P6KpHov9DUieF/ZZbfGpTiruOYCq4nKu8Q4masOuswlOl3nNKZa0C3u4JG+TPubjslYH9A== + dependencies: + "@corex/deepmerge" "^2.6.148" + minimist "^1.2.6" + next@12.0.7: version "12.0.7" resolved "https://registry.yarnpkg.com/next/-/next-12.0.7.tgz#33ebf229b81b06e583ab5ae7613cffe1ca2103fc" From 467f7ded73e923f6c741b68c317904f113d9c632 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 24 Mar 2022 09:40:57 -0700 Subject: [PATCH 38/45] Update API link --- web/next.config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/next.config.js b/web/next.config.js index f030ed91..a40c9e32 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -1,5 +1,4 @@ -const API_DOCS_URL = - 'https://manifoldmarkets.notion.site/Manifold-Markets-API-5e7d0aef4dcf452bb04b319e178fabc5' +const API_DOCS_URL = 'https://docs.manifold.markets/api' /** @type {import('next').NextConfig} */ module.exports = { From b6281b0b56349e5ec4a877bd25787238d1faa0dd Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 24 Mar 2022 09:52:13 -0700 Subject: [PATCH 39/45] Generate sitemap server-side --- web/next-sitemap.js | 40 +++++--------------------------- web/pages/server-sitemap.xml.tsx | 21 +++++++++++++++++ web/public/robots.txt | 10 ++++++++ web/public/sitemap-0.xml | 19 +++++++++++++++ web/public/sitemap.xml | 4 ++++ 5 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 web/pages/server-sitemap.xml.tsx create mode 100644 web/public/robots.txt create mode 100644 web/public/sitemap-0.xml create mode 100644 web/public/sitemap.xml diff --git a/web/next-sitemap.js b/web/next-sitemap.js index 3298f52b..cd6c9c35 100644 --- a/web/next-sitemap.js +++ b/web/next-sitemap.js @@ -1,43 +1,15 @@ -const https = require('https') - /** @type {import('next-sitemap').IConfig} */ module.exports = { siteUrl: process.env.SITE_URL || 'https://manifold.markets', changefreq: 'hourly', priority: 0.7, // Set high priority by default - additionalPaths, - exclude: ['/admin'], + exclude: ['/admin', '/server-sitemap.xml'], generateRobotsTxt: true, + robotsTxtOptions: { + additionalSitemaps: [ + 'https://manifold.markets/server-sitemap.xml', // <==== Add here + ], + }, // Other options: https://github.com/iamvishnusankar/next-sitemap#configuration-options } - -// See https://github.com/iamvishnusankar/next-sitemap#additional-paths-function -async function additionalPaths(config) { - // Fetching data from https://docs.manifold.markets/api - const response = await fetch(`${config.siteUrl}/api/v0/markets`) - - const liteMarkets = await response - // See https://www.sitemaps.org/protocol.html - return liteMarkets.map((liteMarket) => ({ - loc: liteMarket.url, - changefreq: 'hourly', - priority: 0.2, // Individual markets aren't that important - // TODO: Add `lastmod` aka last modified time - })) -} - -// Polyfill for fetch: get the JSON contents of a URL -async function fetch(url) { - return new Promise((resolve, reject) => { - https.get(url, (res) => { - let data = '' - res.on('data', (chunk) => { - data += chunk - }) - res.on('end', () => { - resolve(JSON.parse(data)) - }) - }) - }) -} diff --git a/web/pages/server-sitemap.xml.tsx b/web/pages/server-sitemap.xml.tsx new file mode 100644 index 00000000..8625860c --- /dev/null +++ b/web/pages/server-sitemap.xml.tsx @@ -0,0 +1,21 @@ +import { GetServerSideProps } from 'next' +import { getServerSideSitemap } from 'next-sitemap' +import { DOMAIN } from '../../common/envs/constants' + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + // Fetching data from https://docs.manifold.markets/api + const response = await fetch(`https://${DOMAIN}/api/v0/markets`) + + const liteMarkets = await response.json() + const fields = liteMarkets.map((liteMarket: any) => ({ + // See https://www.sitemaps.org/protocol.html + loc: liteMarket.url, + changefreq: 'hourly', + priority: 0.2, // Individual markets aren't that important + // TODO: Add `lastmod` aka last modified time + })) + return getServerSideSitemap(ctx, fields) +} + +// Default export to prevent next.js errors +export default function Sitemap() {} diff --git a/web/public/robots.txt b/web/public/robots.txt new file mode 100644 index 00000000..014904fd --- /dev/null +++ b/web/public/robots.txt @@ -0,0 +1,10 @@ +# * +User-agent: * +Allow: / + +# Host +Host: https://manifold.markets + +# Sitemaps +Sitemap: https://manifold.markets/sitemap.xml +Sitemap: https://manifold.markets/server-sitemap.xml diff --git a/web/public/sitemap-0.xml b/web/public/sitemap-0.xml new file mode 100644 index 00000000..9db55606 --- /dev/null +++ b/web/public/sitemap-0.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"> +<url><loc>https://manifold.markets</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/about</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/account</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/add-funds</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/analytics</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/create</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/embed/analytics</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/folds</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/home</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/landing-page</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/leaderboards</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/make-predictions</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/markets</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/profile</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/simulator</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +<url><loc>https://manifold.markets/trades</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url> +</urlset> \ No newline at end of file diff --git a/web/public/sitemap.xml b/web/public/sitemap.xml new file mode 100644 index 00000000..050639f2 --- /dev/null +++ b/web/public/sitemap.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> +<sitemap><loc>https://manifold.markets/sitemap-0.xml</loc></sitemap> +</sitemapindex> \ No newline at end of file From 7d8a87615a5c4c68ecd3e26b261ffad4bf05accb Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 24 Mar 2022 12:03:08 -0500 Subject: [PATCH 40/45] Embed market: dynamically adjust graph height --- common/util/array.ts | 2 +- web/components/answers/answers-graph.tsx | 5 +- web/components/contract-prob-graph.tsx | 5 +- web/components/layout/col.tsx | 7 ++- web/hooks/use-measure-size.ts | 63 +++++++++++++++++++ web/pages/embed/[username]/[contractSlug].tsx | 26 ++++++-- 6 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 web/hooks/use-measure-size.ts diff --git a/common/util/array.ts b/common/util/array.ts index fba342aa..d81edba1 100644 --- a/common/util/array.ts +++ b/common/util/array.ts @@ -1,3 +1,3 @@ export function filterDefined<T>(array: (T | null | undefined)[]) { - return array.filter((item) => item) as T[] + return array.filter((item) => item !== null && item !== undefined) as T[] } diff --git a/web/components/answers/answers-graph.tsx b/web/components/answers/answers-graph.tsx index a602a826..5c5afd13 100644 --- a/web/components/answers/answers-graph.tsx +++ b/web/components/answers/answers-graph.tsx @@ -14,8 +14,9 @@ const NUM_LINES = 6 export function AnswersGraph(props: { contract: FullContract<DPM, FreeResponse> bets: Bet[] + height?: number }) { - const { contract } = props + const { contract, height } = props const { createdTime, resolutionTime, closeTime, answers } = contract const bets = useBets(contract.id) ?? props.bets @@ -86,7 +87,7 @@ export function AnswersGraph(props: { return ( <div className="w-full overflow-hidden" - style={{ height: !width || width >= 800 ? 350 : 225 }} + style={{ height: height ?? (!width || width >= 800 ? 350 : 225) }} > <ResponsiveLine data={data} diff --git a/web/components/contract-prob-graph.tsx b/web/components/contract-prob-graph.tsx index 4513c339..36700f4e 100644 --- a/web/components/contract-prob-graph.tsx +++ b/web/components/contract-prob-graph.tsx @@ -10,8 +10,9 @@ import { useWindowSize } from '../hooks/use-window-size' export function ContractProbGraph(props: { contract: FullContract<DPM | CPMM, Binary> bets: Bet[] + height?: number }) { - const { contract } = props + const { contract, height } = props const { resolutionTime, closeTime } = contract const bets = useBetsWithoutAntes(contract, props.bets).filter( @@ -63,7 +64,7 @@ export function ContractProbGraph(props: { return ( <div className="w-full overflow-hidden" - style={{ height: !width || width >= 800 ? 400 : 250 }} + style={{ height: height ?? (!width || width >= 800 ? 400 : 250) }} > <ResponsiveLine data={data} diff --git a/web/components/layout/col.tsx b/web/components/layout/col.tsx index d5f005ca..00e85532 100644 --- a/web/components/layout/col.tsx +++ b/web/components/layout/col.tsx @@ -1,15 +1,16 @@ import clsx from 'clsx' -import { CSSProperties } from 'react' +import { CSSProperties, Ref } from 'react' export function Col(props: { children?: any className?: string style?: CSSProperties + ref?: Ref<HTMLDivElement> }) { - const { children, className, style } = props + const { children, className, style, ref } = props return ( - <div className={clsx(className, 'flex flex-col')} style={style}> + <div className={clsx(className, 'flex flex-col')} style={style} ref={ref}> {children} </div> ) diff --git a/web/hooks/use-measure-size.ts b/web/hooks/use-measure-size.ts new file mode 100644 index 00000000..5e50400b --- /dev/null +++ b/web/hooks/use-measure-size.ts @@ -0,0 +1,63 @@ +import _ from 'lodash' +import { RefObject, useMemo, useLayoutEffect, useRef, useState } from 'react' + +type elem_size = + | { width: number; height: number } + | { width: undefined; height: undefined } + +const getSize = (elem: HTMLElement | null) => + elem + ? { width: elem.offsetWidth, height: elem.offsetHeight } + : { width: undefined, height: undefined } + +export function useListenElemSize<T extends HTMLElement>( + elemRef: RefObject<T | null>, + callback: (size: elem_size) => void, + debounceMs: number | undefined = undefined +) { + const handleResize = useMemo(() => { + let updateSize = () => { + if (elemRef.current) callback(getSize(elemRef.current)) + } + + return debounceMs + ? _.debounce(updateSize, debounceMs, { leading: false, trailing: true }) + : updateSize + }, [callback, elemRef, debounceMs]) + + let elem = elemRef.current + + useLayoutEffect(() => { + if (!elemRef.current) return + + const resizeObserver = new ResizeObserver(handleResize) + resizeObserver.observe(elemRef.current) + + return () => resizeObserver.disconnect() + }, [elemRef, elem, handleResize]) +} + +export function useMeasureSize(debounceMs: number | undefined = undefined) { + const elemRef = useRef<HTMLElement | null>(null) + const [size, setSize] = useState(() => getSize(null)) + const sizeRef = useRef<elem_size>(size) + + const setSizeIfDifferent = (newSize: typeof size) => { + if (newSize?.height !== size?.height || newSize?.width !== size?.width) { + sizeRef.current = newSize + setSize(newSize) + } + } + + useListenElemSize(elemRef, setSizeIfDifferent, debounceMs) + + const setElem = (elem: HTMLElement | null) => { + elemRef.current = elem + + if (elem) { + setSizeIfDifferent(getSize(elem)) + } + } + + return { setElem, elemRef, sizeRef, ...size } +} diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index b5816710..c8d4b046 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -18,7 +18,9 @@ import { Spacer } from '../../../components/layout/spacer' import { Linkify } from '../../../components/linkify' import { SiteLink } from '../../../components/site-link' import { useContractWithPreload } from '../../../hooks/use-contract' +import { useMeasureSize } from '../../../hooks/use-measure-size' import { fromPropz, usePropz } from '../../../hooks/use-propz' +import { useWindowSize } from '../../../hooks/use-window-size' import { listAllBets } from '../../../lib/firebase/bets' import { contractPath, @@ -85,9 +87,18 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { const href = `https://${DOMAIN}${contractPath(contract)}` + const { height: windowHeight } = useWindowSize() + const { setElem, height: topSectionHeight } = useMeasureSize() + const paddingBottom = 8 + + const graphHeight = + windowHeight && topSectionHeight + ? windowHeight - topSectionHeight - paddingBottom + : 0 + return ( - <Col className="w-full flex-1 bg-white py-2"> - <Col className="relative"> + <Col className="w-full flex-1 bg-white"> + <div className="flex flex-col relative pt-2" ref={setElem}> <SiteLink className="absolute top-0 left-0 w-full h-full z-20" href={href} @@ -112,15 +123,20 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { </Row> <Spacer h={2} /> - </Col> + </div> - <div className="mx-1"> + <div className="mx-1" style={{ paddingBottom }}> {isBinary ? ( - <ContractProbGraph contract={contract} bets={bets} /> + <ContractProbGraph + contract={contract} + bets={bets} + height={graphHeight} + /> ) : ( <AnswersGraph contract={contract as FullContract<DPM, FreeResponse>} bets={bets} + height={graphHeight} /> )} </div> From bad58652b8825672e0b96495eadc3e78b5d6bae0 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 24 Mar 2022 22:53:12 -0700 Subject: [PATCH 41/45] Tweak copy --- web/pages/folds.tsx | 3 +-- web/pages/index.tsx | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/pages/folds.tsx b/web/pages/folds.tsx index 0390b006..ae7981dc 100644 --- a/web/pages/folds.tsx +++ b/web/pages/folds.tsx @@ -93,8 +93,7 @@ export default function Folds(props: { <div className="mb-6 text-gray-500"> Communities on Manifold are centered around a collection of - markets. Follow a community to personalize your feed and receive - relevant updates. + markets. Follow a community to personalize your feed! </div> <input diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 28619274..940fa792 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -6,6 +6,7 @@ import { Page } from '../components/page' import { FeedPromo } from '../components/feed-create' import { Col } from '../components/layout/col' import { useUser } from '../hooks/use-user' +import { SiteLink } from '../components/site-link' export async function getStaticProps() { const hotContracts = (await getHotContracts().catch(() => [])) ?? [] @@ -31,6 +32,12 @@ const Home = (props: { hotContracts: Contract[] }) => { <Col className="items-center"> <Col className="max-w-3xl"> <FeedPromo hotContracts={hotContracts ?? []} /> + <p className="mt-6 text-gray-500"> + View{' '} + <SiteLink href="/markets" className="font-bold text-gray-700"> + all markets + </SiteLink> + </p> </Col> </Col> </Page> From 50eb9bd4bd0d268430d8261c11318e15d64194c7 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Fri, 25 Mar 2022 09:27:28 -0700 Subject: [PATCH 42/45] Remove resize handles from input fields (#67) --- web/components/answers/create-answer-panel.tsx | 4 ++-- web/components/feed/feed-items.tsx | 6 +++--- web/pages/create.tsx | 2 +- web/pages/make-predictions.tsx | 2 +- web/pages/profile.tsx | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 9026a666..9da21fc2 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -81,7 +81,7 @@ export function CreateAnswerPanel(props: { <Textarea value={text} onChange={(e) => setText(e.target.value)} - className="textarea textarea-bordered w-full" + className="textarea textarea-bordered w-full resize-none" placeholder="Type your answer..." rows={1} maxLength={10000} @@ -117,7 +117,7 @@ export function CreateAnswerPanel(props: { </Row> </Row> - <Row className="justify-between gap-4 text-sm items-center"> + <Row className="items-center justify-between gap-4 text-sm"> <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500"> <div> Estimated <br /> payout if chosen diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index b85669ba..883b917b 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -237,7 +237,7 @@ export function FeedBet(props: { <Textarea value={comment} onChange={(e) => setComment(e.target.value)} - className="textarea textarea-bordered w-full" + className="textarea textarea-bordered w-full resize-none" placeholder="Add a comment..." rows={3} maxLength={MAX_COMMENT_LENGTH} @@ -278,7 +278,7 @@ function EditContract(props: { return editing ? ( <div className="mt-4"> <Textarea - className="textarea textarea-bordered mb-1 h-24 w-full" + className="textarea textarea-bordered mb-1 h-24 w-full resize-none" rows={3} value={text} onChange={(e) => setText(e.target.value || '')} @@ -707,7 +707,7 @@ function FeedAnswerGroup(props: { </div> <Row className="align-items justify-between gap-4"> - <span className="text-lg whitespace-pre-line"> + <span className="whitespace-pre-line text-lg"> <Linkify text={text} /> </span> diff --git a/web/pages/create.tsx b/web/pages/create.tsx index d6600cd0..78bef6a2 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -184,7 +184,7 @@ export function NewContract(props: { question: string; tag?: string }) { <InfoTooltip text="Optional. Describe how you will resolve this market." /> </label> <Textarea - className="textarea textarea-bordered w-full" + className="textarea textarea-bordered w-full resize-none" rows={3} maxLength={MAX_DESCRIPTION_LENGTH} placeholder={descriptionPlaceholder} diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx index 36d210ef..021c1f6e 100644 --- a/web/pages/make-predictions.tsx +++ b/web/pages/make-predictions.tsx @@ -195,7 +195,7 @@ ${TEST_VALUE} <Textarea placeholder="e.g. This market is part of the ACX predictions for 2022..." - className="input" + className="input resize-none" value={description} onChange={(e) => setDescription(e.target.value || '')} /> diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index 2b3445e5..f637a4f2 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -41,7 +41,7 @@ function EditUserField(props: { {field === 'bio' ? ( <Textarea - className="textarea textarea-bordered w-full" + className="textarea textarea-bordered w-full resize-none" value={value} onChange={(e) => setValue(e.target.value)} onBlur={updateField} From 6e387ef938519e5dfb2dfaaeea30dff27a32e0dc Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Fri, 25 Mar 2022 13:33:57 -0700 Subject: [PATCH 43/45] Keep FR answer panel open after betting (#68) --- web/components/answers/answer-bet-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 4e1f61cb..2cc7d13f 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -63,7 +63,7 @@ export function AnswerBetPanel(props: { if (result?.status === 'success') { setIsSubmitting(false) - closePanel() + setBetAmount(undefined) } else { setError(result?.error || 'Error placing bet') setIsSubmitting(false) From b7d39eaafd15975e24b02990ae135f712149264e Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Fri, 25 Mar 2022 15:31:46 -0700 Subject: [PATCH 44/45] Format large money amounts with 'k' and 'm' --- common/util/format.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/common/util/format.ts b/common/util/format.ts index 05a8f702..0f07c2dd 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -7,10 +7,22 @@ const formatter = new Intl.NumberFormat('en-US', { minimumFractionDigits: 0, }) +// E.g. 1234 => "M$ 1,234"; 23456 => "M$ 23k" export function formatMoney(amount: number) { - const newAmount = Math.round(amount) === 0 ? 0 : amount // handle -0 case + let newAmount = Math.round(amount) === 0 ? 0 : amount // handle -0 case + let suffix = '' + if (newAmount > 10 * 1000) { + suffix = 'k' + newAmount /= 1000 + } else if (newAmount > 10 * 1000 * 1000) { + suffix = 'm' + newAmount /= 1000 * 1000 + } return ( - ENV_CONFIG.moneyMoniker + ' ' + formatter.format(newAmount).replace('$', '') + ENV_CONFIG.moneyMoniker + + ' ' + + formatter.format(newAmount).replace('$', '') + + suffix ) } From 419a3106e8a4443f865a95fc116ac11342583ee3 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Fri, 25 Mar 2022 15:54:15 -0700 Subject: [PATCH 45/45] Revert "Format large money amounts with 'k' and 'm'" This reverts commit b7d39eaafd15975e24b02990ae135f712149264e. --- common/util/format.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/common/util/format.ts b/common/util/format.ts index 0f07c2dd..05a8f702 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -7,22 +7,10 @@ const formatter = new Intl.NumberFormat('en-US', { minimumFractionDigits: 0, }) -// E.g. 1234 => "M$ 1,234"; 23456 => "M$ 23k" export function formatMoney(amount: number) { - let newAmount = Math.round(amount) === 0 ? 0 : amount // handle -0 case - let suffix = '' - if (newAmount > 10 * 1000) { - suffix = 'k' - newAmount /= 1000 - } else if (newAmount > 10 * 1000 * 1000) { - suffix = 'm' - newAmount /= 1000 * 1000 - } + const newAmount = Math.round(amount) === 0 ? 0 : amount // handle -0 case return ( - ENV_CONFIG.moneyMoniker + - ' ' + - formatter.format(newAmount).replace('$', '') + - suffix + ENV_CONFIG.moneyMoniker + ' ' + formatter.format(newAmount).replace('$', '') ) }