diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index 63eaf8aa..e1dc939b 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -120,7 +120,7 @@ export function ContractDetails(props: { {tags.map((tag) => ( -
+
))} diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx index 69908523..e1044d65 100644 --- a/web/components/contracts-list.tsx +++ b/web/components/contracts-list.tsx @@ -11,7 +11,7 @@ import { parseTags } from '../lib/util/parse' import { ContractCard } from './contract-card' import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params' -function ContractsGrid(props: { contracts: Contract[] }) { +export function ContractsGrid(props: { contracts: Contract[] }) { const [resolvedContracts, activeContracts] = _.partition( props.contracts, (c) => c.isResolved diff --git a/web/hooks/use-contracts.ts b/web/hooks/use-contracts.ts index 674c570c..bec304a9 100644 --- a/web/hooks/use-contracts.ts +++ b/web/hooks/use-contracts.ts @@ -1,5 +1,11 @@ +import _ from 'lodash' import { useEffect, useState } from 'react' -import { Contract, listenForContracts } from '../lib/firebase/contracts' +import { Bet, listenForRecentBets } from '../lib/firebase/bets' +import { + computeHotContracts, + Contract, + listenForContracts, +} from '../lib/firebase/contracts' export const useContracts = () => { const [contracts, setContracts] = useState('loading') @@ -10,3 +16,16 @@ export const useContracts = () => { return contracts } + +export const useHotContracts = () => { + const [recentBets, setRecentBets] = useState('loading') + + useEffect(() => { + const oneDay = 1000 * 60 * 60 * 24 + return listenForRecentBets(oneDay, setRecentBets) + }, []) + + if (recentBets === 'loading') return 'loading' + + return computeHotContracts(recentBets) +} diff --git a/web/lib/firebase/bets.ts b/web/lib/firebase/bets.ts index 49d92e0e..818c09bc 100644 --- a/web/lib/firebase/bets.ts +++ b/web/lib/firebase/bets.ts @@ -4,7 +4,9 @@ import { query, onSnapshot, where, + getDocs, } from 'firebase/firestore' +import _ from 'lodash' import { db } from './init' export type Bet = { @@ -62,3 +64,34 @@ export function listenForUserBets( setBets(bets) }) } + +export function listenForRecentBets( + timePeriodMs: number, + setBets: (bets: Bet[]) => void +) { + const recentQuery = query( + collectionGroup(db, 'bets'), + where('createdTime', '>', Date.now() - timePeriodMs) + ) + return onSnapshot(recentQuery, (snap) => { + const bets = snap.docs.map((doc) => doc.data() as Bet) + + bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime) + + setBets(bets) + }) +} + +export async function getRecentBets(timePeriodMs: number) { + const recentQuery = query( + collectionGroup(db, 'bets'), + where('createdTime', '>', Date.now() - timePeriodMs) + ) + + const snapshot = await getDocs(recentQuery) + const bets = snapshot.docs.map((doc) => doc.data() as Bet) + + bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime) + + return bets +} diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 1db5d00e..891831c4 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -11,9 +11,10 @@ import { onSnapshot, orderBy, getDoc, - limit, } from 'firebase/firestore' import dayjs from 'dayjs' +import { Bet, getRecentBets } from './bets' +import _ from 'lodash' export type Contract = { id: string @@ -131,3 +132,17 @@ export function listenForContract( setContract((contractSnap.data() ?? null) as Contract | null) }) } + +export function computeHotContracts(recentBets: Bet[]) { + const contractBets = _.groupBy(recentBets, (bet) => bet.contractId) + const hotContractIds = _.sortBy(Object.keys(contractBets), (contractId) => + _.sumBy(contractBets[contractId], (bet) => -1 * bet.amount) + ).slice(0, 4) + return hotContractIds +} + +export async function getHotContracts() { + const oneDay = 1000 * 60 * 60 * 24 + const recentBets = await getRecentBets(oneDay) + return computeHotContracts(recentBets) +} diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 6e4ba609..cd409aba 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -3,26 +3,42 @@ import React from 'react' import { useUser } from '../hooks/use-user' import Markets from './markets' import LandingPage from './landing-page' -import { Contract, listAllContracts } from '../lib/firebase/contracts' +import { + Contract, + getHotContracts, + listAllContracts, +} from '../lib/firebase/contracts' +import _ from 'lodash' export async function getStaticProps() { - const contracts = await listAllContracts().catch((_) => []) + const [contracts, hotContractIds] = await Promise.all([ + listAllContracts().catch((_) => []), + getHotContracts().catch(() => []), + ]) return { props: { contracts, + hotContractIds, }, revalidate: 60, // regenerate after a minute } } -const Home = (props: { contracts: Contract[] }) => { +const Home = (props: { contracts: Contract[]; hotContractIds: string[] }) => { const user = useUser() if (user === undefined) return <> - return user ? : + return user ? ( + + ) : ( + + ) } export default Home diff --git a/web/pages/markets.tsx b/web/pages/markets.tsx index 96f3c7a4..f28c493d 100644 --- a/web/pages/markets.tsx +++ b/web/pages/markets.tsx @@ -1,30 +1,61 @@ -import { SearchableGrid } from '../components/contracts-list' +import _ from 'lodash' +import { ContractsGrid, SearchableGrid } from '../components/contracts-list' +import { Spacer } from '../components/layout/spacer' import { Page } from '../components/page' -import { useContracts } from '../hooks/use-contracts' +import { Title } from '../components/title' +import { useContracts, useHotContracts } from '../hooks/use-contracts' import { useQueryAndSortParams } from '../hooks/use-sort-and-query-params' -import { Contract, listAllContracts } from '../lib/firebase/contracts' +import { + Contract, + getHotContracts, + listAllContracts, +} from '../lib/firebase/contracts' export async function getStaticProps() { - const contracts = await listAllContracts().catch((_) => []) + const [contracts, hotContractIds] = await Promise.all([ + listAllContracts().catch((_) => []), + getHotContracts().catch(() => []), + ]) return { props: { contracts, + hotContractIds, }, revalidate: 60, // regenerate after a minute } } -export default function Markets(props: { contracts: Contract[] }) { +export default function Markets(props: { + contracts: Contract[] + hotContractIds: string[] +}) { const contracts = useContracts() const { query, setQuery, sort, setSort } = useQueryAndSortParams() + const hotContractIds = useHotContracts() + + const readyHotContractIds = + hotContractIds === 'loading' ? props.hotContractIds : hotContractIds + const readyContracts = contracts === 'loading' ? props.contracts : contracts + + const hotContracts = readyHotContractIds.map( + (hotId) => + _.find(readyContracts, (contract) => contract.id === hotId) as Contract + ) return ( +
+ + <ContractsGrid contracts={hotContracts} /> + </div> + + <Spacer h={10} /> + {(props.contracts || contracts !== 'loading') && ( <SearchableGrid - contracts={contracts === 'loading' ? props.contracts : contracts} + contracts={readyContracts} query={query} setQuery={setQuery} sort={sort}