diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index dd412149..5da5b272 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -8,6 +8,7 @@ import { Bet } from '../../common/bet' import { getUser, payUser } from './utils' import { sendMarketResolutionEmail } from './emails' import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts' +import { removeUndefinedProps } from '../../common/util/object' export const resolveMarket = functions .runWith({ minInstances: 1 }) @@ -31,7 +32,7 @@ export const resolveMarket = functions if (!contractSnap.exists) return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract - const { creatorId, outcomeType } = contract + const { creatorId, outcomeType, closeTime } = contract if (outcomeType === 'BINARY') { if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome)) @@ -68,15 +69,21 @@ export const resolveMarket = functions const resolutionProbability = probabilityInt !== undefined ? probabilityInt / 100 : undefined - await contractDoc.update({ - isResolved: true, - resolution: outcome, - resolutionTime: Date.now(), - ...(resolutionProbability === undefined - ? {} - : { resolutionProbability }), - ...(resolutions === undefined ? {} : { resolutions }), - }) + const resolutionTime = Date.now() + const newCloseTime = closeTime + ? Math.min(closeTime, resolutionTime) + : closeTime + + await contractDoc.update( + removeUndefinedProps({ + isResolved: true, + resolution: outcome, + resolutionTime, + closeTime: newCloseTime, + resolutionProbability, + resolutions, + }) + ) console.log('contract ', contractId, 'resolved to:', outcome) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index c9822688..4638435e 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -157,8 +157,8 @@ export function BetsList(props: { user: User }) { > - - + + @@ -358,22 +358,26 @@ export function MyBetsSummary(props: { {formatMoney(expectation)} */} - -
- Payout if -
-
- {formatMoney(yesWinnings)} -
- - -
- Payout if -
-
- {formatMoney(noWinnings)} -
- + {isBinary && ( + <> + +
+ Payout if +
+
+ {formatMoney(yesWinnings)} +
+ + +
+ Payout if +
+
+ {formatMoney(noWinnings)} +
+ + + )}
{isBinary ? ( @@ -418,9 +422,10 @@ export function ContractBetsTable(props: { - {isResolved ? <>Payout : <>Sale price} Outcome Amount + {isResolved ? <>Payout : <>Sale price} + {!isResolved && Payout if chosen} Probability Shares Date @@ -471,6 +476,11 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) { ) ) + const payoutIfChosenDisplay = + bet.outcome === '0' && bet.isAnte + ? 'N/A' + : formatMoney(calculatePayout(contract, bet, bet.outcome)) + return ( @@ -478,11 +488,12 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) { )} - {saleDisplay} {formatMoney(amount)} + {saleDisplay} + {!isResolved && {payoutIfChosenDisplay}} {formatPercent(probBefore)} → {formatPercent(probAfter)} @@ -499,6 +510,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) { }, []) const { contract, bet } = props + const isBinary = contract.outcomeType === 'BINARY' const [isSubmitting, setIsSubmitting] = useState(false) const initialProb = getOutcomeProbability( @@ -537,8 +549,9 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
- Implied probability: {formatPercent(initialProb)} →{' '} - {formatPercent(outcomeProb)} + ({isBinary ? 'Updated' : }{' '} + probability: {formatPercent(initialProb)} → {formatPercent(outcomeProb)} + )
) diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index 58dd5f01..c582a043 100644 --- a/web/components/contract-feed.tsx +++ b/web/components/contract-feed.tsx @@ -790,3 +790,27 @@ export function ContractFeed(props: { ) } + +export function ContractSummaryFeed(props: { + contract: Contract + betRowClassName?: string +}) { + const { contract, betRowClassName } = props + const { outcomeType } = contract + const isBinary = outcomeType === 'BINARY' + + return ( +
+
+
+
+ +
+
+
+ {isBinary && tradingAllowed(contract) && ( + + )} +
+ ) +} diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx index 6fc49780..911e546e 100644 --- a/web/components/contracts-list.tsx +++ b/web/components/contracts-list.tsx @@ -205,9 +205,11 @@ export function SearchableGrid(props: { }) { const { contracts, query, setQuery, sort, setSort, byOneCreator } = props + const queryWords = query.toLowerCase().split(' ') function check(corpus: String) { - return corpus.toLowerCase().includes(query.toLowerCase()) + return queryWords.every((word) => corpus.toLowerCase().includes(word)) } + let matches = contracts.filter( (c) => check(c.question) || diff --git a/web/components/fast-fold-following.tsx b/web/components/fast-fold-following.tsx index d4d350ee..577719b6 100644 --- a/web/components/fast-fold-following.tsx +++ b/web/components/fast-fold-following.tsx @@ -32,7 +32,7 @@ function FollowFoldButton(props: { className={clsx( 'rounded-full border-2 px-4 py-1 shadow-md', 'cursor-pointer', - followed ? 'bg-gray-300 border-gray-300' : 'bg-white' + followed ? 'border-gray-300 bg-gray-300' : 'bg-white' )} onClick={onClick} > @@ -101,7 +101,7 @@ export const FastFoldFollowing = (props: { ]} /> - + ) } diff --git a/web/components/profile-menu.tsx b/web/components/profile-menu.tsx index d1a42cd8..7cc00e6a 100644 --- a/web/components/profile-menu.tsx +++ b/web/components/profile-menu.tsx @@ -54,6 +54,10 @@ function getNavigationOptions( name: 'Your trades', href: '/trades', }, + { + name: 'Add funds', + href: '/add-funds', + }, { name: 'Leaderboards', href: '/leaderboards', diff --git a/web/hooks/use-contracts.ts b/web/hooks/use-contracts.ts index 60744c33..af36cd82 100644 --- a/web/hooks/use-contracts.ts +++ b/web/hooks/use-contracts.ts @@ -5,6 +5,7 @@ import { listenForActiveContracts, listenForContracts, listenForHotContracts, + listenForInactiveContracts, } from '../lib/firebase/contracts' import { listenForTaggedContracts } from '../lib/firebase/folds' @@ -28,6 +29,16 @@ export const useActiveContracts = () => { return contracts } +export const useInactiveContracts = () => { + const [contracts, setContracts] = useState() + + useEffect(() => { + return listenForInactiveContracts(setContracts) + }, []) + + return contracts +} + export const useUpdatedContracts = (initialContracts: Contract[]) => { const [contracts, setContracts] = useState(initialContracts) diff --git a/web/hooks/use-active-contracts.ts b/web/hooks/use-find-active-contracts.ts similarity index 66% rename from web/hooks/use-active-contracts.ts rename to web/hooks/use-find-active-contracts.ts index 1c4c1b91..f8aa5627 100644 --- a/web/hooks/use-active-contracts.ts +++ b/web/hooks/use-find-active-contracts.ts @@ -1,5 +1,5 @@ import _ from 'lodash' -import { useRef } from 'react' +import { useMemo, useRef } from 'react' import { Fold } from '../../common/fold' import { User } from '../../common/user' @@ -9,7 +9,7 @@ import { Comment, getRecentComments } from '../lib/firebase/comments' import { Contract, getActiveContracts } from '../lib/firebase/contracts' import { listAllFolds } from '../lib/firebase/folds' import { findActiveContracts } from '../pages/activity' -import { useActiveContracts } from './use-contracts' +import { useInactiveContracts } from './use-contracts' import { useFollowedFolds } from './use-fold' import { useUserBetContracts } from './use-user-bets' @@ -28,24 +28,15 @@ export const getAllContractInfo = async () => { return { contracts, recentBets, recentComments, folds } } -export const useFindActiveContracts = ( - props: { - contracts: Contract[] - folds: Fold[] - recentBets: Bet[] - recentComments: Comment[] - }, - user: User | undefined | null +export const useFilterYourContracts = ( + user: User | undefined | null, + folds: Fold[], + contracts: Contract[] ) => { - const { recentBets, recentComments } = props - const contracts = useActiveContracts() ?? props.contracts - const followedFoldIds = useFollowedFolds(user) const followedFolds = filterDefined( - (followedFoldIds ?? []).map((id) => - props.folds.find((fold) => fold.id === id) - ) + (followedFoldIds ?? []).map((id) => folds.find((fold) => fold.id === id)) ) // Save the initial followed fold slugs. @@ -64,20 +55,33 @@ export const useFindActiveContracts = ( : undefined // Show no contracts before your info is loaded. - let feedContracts: Contract[] = [] + let yourContracts: Contract[] = [] if (yourBetContracts && followedFoldIds) { // Show all contracts if no folds are followed. - if (followedFoldIds.length === 0) feedContracts = contracts + if (followedFoldIds.length === 0) yourContracts = contracts else - feedContracts = contracts.filter( + yourContracts = contracts.filter( (contract) => contract.lowercaseTags.some((tag) => tagSet.has(tag)) || yourBetContracts.has(contract.id) ) } + return { + yourContracts, + initialFollowedFoldSlugs, + } +} + +export const useFindActiveContracts = (props: { + contracts: Contract[] + recentBets: Bet[] + recentComments: Comment[] +}) => { + const { contracts, recentBets, recentComments } = props + const activeContracts = findActiveContracts( - feedContracts, + contracts, recentComments, recentBets ) @@ -101,6 +105,24 @@ export const useFindActiveContracts = ( activeContracts, activeBets, activeComments, - initialFollowedFoldSlugs, } } + +export const useExploreContracts = (maxContracts = 75) => { + const inactiveContracts = useInactiveContracts() + + const contractsDict = _.fromPairs( + (inactiveContracts ?? []).map((c) => [c.id, c]) + ) + + // Preserve random ordering once inactiveContracts loaded. + const exploreContractIds = useMemo( + () => _.shuffle(Object.keys(contractsDict)), + // eslint-disable-next-line react-hooks/exhaustive-deps + [!!inactiveContracts] + ).slice(0, maxContracts) + + if (!inactiveContracts) return undefined + + return filterDefined(exploreContractIds.map((id) => contractsDict[id])) +} diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index b22a547f..eb1b65e1 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -115,7 +115,7 @@ export function listenForContracts( return listenForValues(q, setContracts) } -const activeContracts = query( +const activeContractsQuery = query( contractCollection, where('isResolved', '==', false), where('visibility', '==', 'public'), @@ -123,13 +123,31 @@ const activeContracts = query( ) export function getActiveContracts() { - return getValues(activeContracts) + return getValues(activeContractsQuery) } export function listenForActiveContracts( setContracts: (contracts: Contract[]) => void ) { - return listenForValues(activeContracts, setContracts) + return listenForValues(activeContractsQuery, setContracts) +} + +const inactiveContractsQuery = query( + contractCollection, + where('isResolved', '==', false), + where('closeTime', '>', Date.now()), + where('visibility', '==', 'public'), + where('volume24Hours', '==', 0) +) + +export function getInactiveContracts() { + return getValues(inactiveContractsQuery) +} + +export function listenForInactiveContracts( + setContracts: (contracts: Contract[]) => void +) { + return listenForValues(inactiveContractsQuery, setContracts) } export function listenForContract( diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index f90da12e..076c4939 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -188,12 +188,8 @@ function BetsSection(props: { return (
- {isBinary && ( - <> - <MyBetsSummary className="px-2" contract={contract} bets={userBets} /> - <Spacer h={6} /> - </> - )} + <MyBetsSummary className="px-2" contract={contract} bets={userBets} /> + <Spacer h={6} /> <ContractBetsTable contract={contract} bets={userBets} /> <Spacer h={12} /> </div> diff --git a/web/pages/about.tsx b/web/pages/about.tsx index c818be8e..85e3eaee 100644 --- a/web/pages/about.tsx +++ b/web/pages/about.tsx @@ -139,6 +139,15 @@ function Contents() { bettors that are correct more often will gain influence, leading to better-calibrated forecasts over time. </p> + <p> + Since our launch, we've seen hundreds of users trade each day, on over a + thousand different markets! You can track the popularity of our platform + at{' '} + <a href="http://manifold.markets/analytics"> + http://manifold.markets/analytics + </a> + . + </p> <h3 id="how-are-markets-resolved-">How are markets resolved?</h3> <p> The creator of the prediction market decides the outcome and earns{' '} diff --git a/web/pages/activity.tsx b/web/pages/activity.tsx index e546e1d1..bab58328 100644 --- a/web/pages/activity.tsx +++ b/web/pages/activity.tsx @@ -1,5 +1,5 @@ import _ from 'lodash' -import { ContractFeed } from '../components/contract-feed' +import { ContractFeed, ContractSummaryFeed } from '../components/contract-feed' import { Page } from '../components/page' import { Contract } from '../lib/firebase/contracts' import { Comment } from '../lib/firebase/comments' @@ -99,6 +99,24 @@ export function ActivityFeed(props: { ) } +export function SummaryActivityFeed(props: { contracts: Contract[] }) { + const { contracts } = props + + return ( + <Col className="items-center"> + <Col className="w-full max-w-3xl"> + <Col className="w-full divide-y divide-gray-300 self-center bg-white"> + {contracts.map((contract) => ( + <div key={contract.id} className="py-6 px-2 sm:px-4"> + <ContractSummaryFeed contract={contract} /> + </div> + ))} + </Col> + </Col> + </Col> + ) +} + export default function ActivityPage() { return ( <Page> diff --git a/web/pages/add-funds.tsx b/web/pages/add-funds.tsx index dcf65088..339be265 100644 --- a/web/pages/add-funds.tsx +++ b/web/pages/add-funds.tsx @@ -19,11 +19,11 @@ export default function AddFundsPage() { <SEO title="Add funds" description="Add funds" url="/add-funds" /> <Col className="items-center"> - <Col> - <Title text="Get Manifold Dollars" /> + <Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md"> + <Title className="!mt-0" text="Get Manifold Dollars" /> <img - className="mt-6 block" - src="/praying-mantis-light.svg" + className="mb-6 block -scale-x-100 self-center" + src="/stylized-crane-black.png" width={200} height={200} /> @@ -50,7 +50,7 @@ export default function AddFundsPage() { <form action={checkoutURL(user?.id || '', amountSelected)} method="POST" - className="mt-12" + className="mt-8" > <button type="submit" diff --git a/web/pages/api/v0/_types.ts b/web/pages/api/v0/_types.ts index 6dc4b245..44a6119e 100644 --- a/web/pages/api/v0/_types.ts +++ b/web/pages/api/v0/_types.ts @@ -26,6 +26,7 @@ export type LiteMarket = { volume24Hours: number isResolved: boolean resolution?: string + resolutionTime?: number } export type FullMarket = LiteMarket & { @@ -54,6 +55,7 @@ export function toLiteMarket({ volume24Hours, isResolved, resolution, + resolutionTime, }: Contract): LiteMarket { return { id, @@ -61,7 +63,10 @@ export function toLiteMarket({ creatorName, createdTime, creatorAvatarUrl, - closeTime, + closeTime: + resolutionTime && closeTime + ? Math.min(resolutionTime, closeTime) + : closeTime, question, description, tags, @@ -72,5 +77,6 @@ export function toLiteMarket({ volume24Hours, isResolved, resolution, + resolutionTime, } } diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx index 9040a3b7..f15cdcff 100644 --- a/web/pages/fold/[...slugs]/index.tsx +++ b/web/pages/fold/[...slugs]/index.tsx @@ -41,6 +41,7 @@ import { SEO } from '../../../components/SEO' import { useTaggedContracts } from '../../../hooks/use-contracts' import { Linkify } from '../../../components/linkify' import { usePropz } from '../../../hooks/use-propz' +import { filterDefined } from '../../../../common/util/array' export async function getStaticPropz(props: { params: { slugs: string[] } }) { const { slugs } = props.params @@ -182,9 +183,11 @@ export default function FoldPage(props: { taggedContracts.map((contract) => [contract.id, contract]) ) - const contracts = props.contracts.map((contract) => contractsMap[contract.id]) - const activeContracts = props.activeContracts.map( - (contract) => contractsMap[contract.id] + const contracts = filterDefined( + props.contracts.map((contract) => contractsMap[contract.id]) + ) + const activeContracts = filterDefined( + props.activeContracts.map((contract) => contractsMap[contract.id]) ) if (fold === null || !foldSubpages.includes(page) || slugs[2]) { diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 64543e67..664489e6 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -1,9 +1,12 @@ -import React from 'react' +import React, { useState } from 'react' import Router from 'next/router' +import { SparklesIcon, GlobeAltIcon } from '@heroicons/react/solid' +import clsx from 'clsx' +import _ from 'lodash' import { Contract } from '../lib/firebase/contracts' import { Page } from '../components/page' -import { ActivityFeed } from './activity' +import { ActivityFeed, SummaryActivityFeed } from './activity' import { Comment } from '../lib/firebase/comments' import { Bet } from '../lib/firebase/bets' import FeedCreate from '../components/feed-create' @@ -13,14 +16,16 @@ import { useUser } from '../hooks/use-user' import { Fold } from '../../common/fold' import { LoadingIndicator } from '../components/loading-indicator' import { Row } from '../components/layout/row' -import { SparklesIcon } from '@heroicons/react/solid' import { FastFoldFollowing } from '../components/fast-fold-following' import { getAllContractInfo, + useExploreContracts, + useFilterYourContracts, useFindActiveContracts, -} from '../hooks/use-active-contracts' +} from '../hooks/use-find-active-contracts' import { useGetRecentBets } from '../hooks/use-bets' import { usePropz } from '../hooks/use-propz' +import { useActiveContracts } from '../hooks/use-contracts' export async function getStaticPropz() { const contractInfo = await getAllContractInfo() @@ -42,21 +47,28 @@ const Home = (props: { folds: [], recentComments: [], } - const { contracts, folds, recentComments } = props + const { folds, recentComments } = props const user = useUser() - const recentBets = useGetRecentBets() - - const { - activeContracts, - activeBets, - activeComments, - initialFollowedFoldSlugs, - } = useFindActiveContracts( - { contracts, folds, recentBets: recentBets ?? [], recentComments }, - user + const contracts = useActiveContracts() ?? props.contracts + const { yourContracts, initialFollowedFoldSlugs } = useFilterYourContracts( + user, + folds, + contracts ) + const recentBets = useGetRecentBets() + const { activeContracts, activeBets, activeComments } = + useFindActiveContracts({ + contracts: yourContracts, + recentBets: recentBets ?? [], + recentComments, + }) + + const exploreContracts = useExploreContracts() + + const [feedMode, setFeedMode] = useState<'activity' | 'explore'>('activity') + if (user === null) { Router.replace('/') return <></> @@ -77,22 +89,52 @@ const Home = (props: { /> )} + <Spacer h={5} /> + <Col className="mx-3 mb-3 gap-2 text-sm text-gray-800 sm:flex-row"> <Row className="gap-2"> - <SparklesIcon className="inline h-5 w-5" aria-hidden="true" /> - <span className="whitespace-nowrap">Recent activity</span> + <div className="tabs"> + <div + className={clsx( + 'tab gap-2', + feedMode === 'activity' && 'tab-active' + )} + onClick={() => setFeedMode('activity')} + > + <SparklesIcon className="inline h-5 w-5" aria-hidden="true" /> + Recent activity + </div> + <div + className={clsx( + 'tab gap-2', + feedMode === 'explore' && 'tab-active' + )} + onClick={() => setFeedMode('explore')} + > + <GlobeAltIcon className="inline h-5 w-5" aria-hidden="true" /> + Explore + </div> + </div> </Row> </Col> - {activeContracts && recentBets ? ( - <ActivityFeed - contracts={activeContracts} - contractBets={activeBets} - contractComments={activeComments} - /> - ) : ( - <LoadingIndicator className="mt-4" /> - )} + {feedMode === 'activity' && + (recentBets ? ( + <ActivityFeed + contracts={activeContracts} + contractBets={activeBets} + contractComments={activeComments} + /> + ) : ( + <LoadingIndicator className="mt-4" /> + ))} + + {feedMode === 'explore' && + (exploreContracts ? ( + <SummaryActivityFeed contracts={exploreContracts} /> + ) : ( + <LoadingIndicator className="mt-4" /> + ))} </Col> </Col> </Page> diff --git a/web/public/stylized-crane-black.png b/web/public/stylized-crane-black.png new file mode 100644 index 00000000..4bdf2bc6 Binary files /dev/null and b/web/public/stylized-crane-black.png differ