Make tournament page efficient (#832)

* Make tournament page efficient

* Fix URL to Salem contract

* Use totalMembers instead of deprecated field

* Increase page size to 12

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
This commit is contained in:
Marshall Polaris 2022-09-03 09:55:10 -07:00 committed by GitHub
parent 0938368e30
commit c0383bcf26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 137 additions and 120 deletions

View File

@ -33,7 +33,7 @@ export function Carousel(props: {
}, 500) }, 500)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(onScroll, []) useEffect(onScroll, [children])
return ( return (
<div className={clsx('relative', className)}> <div className={clsx('relative', className)}>

View File

@ -103,6 +103,7 @@ export const usePagination = <T>(opts: PaginationOptions<T>) => {
isEnd: state.isComplete && state.pageEnd >= state.docs.length, isEnd: state.isComplete && state.pageEnd >= state.docs.length,
getPrev: () => dispatch({ type: 'PREV' }), getPrev: () => dispatch({ type: 'PREV' }),
getNext: () => dispatch({ type: 'NEXT' }), getNext: () => dispatch({ type: 'NEXT' }),
allItems: () => state.docs.map((d) => d.data()),
getItems: () => getItems: () =>
state.docs.slice(state.pageStart, state.pageEnd).map((d) => d.data()), state.docs.slice(state.pageStart, state.pageEnd).map((d) => d.data()),
} }

View File

@ -104,11 +104,18 @@ export async function listContracts(creatorId: string): Promise<Contract[]> {
return snapshot.docs.map((doc) => doc.data()) return snapshot.docs.map((doc) => doc.data())
} }
export const contractsByGroupSlugQuery = (slug: string) =>
query(
contracts,
where('groupSlugs', 'array-contains', slug),
where('isResolved', '==', false),
orderBy('popularityScore', 'desc')
)
export async function listContractsByGroupSlug( export async function listContractsByGroupSlug(
slug: string slug: string
): Promise<Contract[]> { ): Promise<Contract[]> {
const q = query(contracts, where('groupSlugs', 'array-contains', slug)) const snapshot = await getDocs(contractsByGroupSlugQuery(slug))
const snapshot = await getDocs(q)
return snapshot.docs.map((doc) => doc.data()) return snapshot.docs.map((doc) => doc.data())
} }

View File

@ -1,16 +1,10 @@
import { ClockIcon } from '@heroicons/react/outline' import { ClockIcon } from '@heroicons/react/outline'
import { UsersIcon } from '@heroicons/react/solid' import { UsersIcon } from '@heroicons/react/solid'
import { import dayjs from 'dayjs'
BinaryContract,
Contract,
PseudoNumericContract,
} from 'common/contract'
import { Group } from 'common/group'
import dayjs, { Dayjs } from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat' import customParseFormat from 'dayjs/plugin/customParseFormat'
import timezone from 'dayjs/plugin/timezone' import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import { keyBy, mapValues, sortBy } from 'lodash' import { zip } from 'lodash'
import Image, { ImageProps, StaticImageData } from 'next/image' import Image, { ImageProps, StaticImageData } from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { useState } from 'react'
@ -20,27 +14,33 @@ import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { SEO } from 'web/components/SEO' import { SEO } from 'web/components/SEO'
import { listContractsByGroupSlug } from 'web/lib/firebase/contracts' import { contractsByGroupSlugQuery } from 'web/lib/firebase/contracts'
import { getGroup, groupPath } from 'web/lib/firebase/groups' import { getGroup, groupPath } from 'web/lib/firebase/groups'
import elon_pic from './_cspi/Will_Elon_Buy_Twitter.png' import elon_pic from './_cspi/Will_Elon_Buy_Twitter.png'
import china_pic from './_cspi/Chinese_Military_Action_against_Taiwan.png' import china_pic from './_cspi/Chinese_Military_Action_against_Taiwan.png'
import mpox_pic from './_cspi/Monkeypox_Cases.png' import mpox_pic from './_cspi/Monkeypox_Cases.png'
import race_pic from './_cspi/Supreme_Court_Ban_Race_in_College_Admissions.png' import race_pic from './_cspi/Supreme_Court_Ban_Race_in_College_Admissions.png'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { getProbability } from 'common/calculate'
import { Carousel } from 'web/components/carousel' import { Carousel } from 'web/components/carousel'
import { usePagination } from 'web/hooks/use-pagination'
import { LoadingIndicator } from 'web/components/loading-indicator'
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const toDate = (d: string) => dayjs(d, 'MMM D, YYYY').tz('America/Los_Angeles') const toDate = (d: string) =>
dayjs(d, 'MMM D, YYYY').tz('America/Los_Angeles').valueOf()
type MarketImage = {
marketUrl: string
image: StaticImageData
}
type Tourney = { type Tourney = {
title: string title: string
url?: string
blurb: string // actual description in the click-through blurb: string // actual description in the click-through
award?: string award?: string
endTime?: Dayjs endTime?: number
groupId: string groupId: string
} }
@ -50,7 +50,7 @@ const Salem = {
url: 'https://salemcenter.manifold.markets/', url: 'https://salemcenter.manifold.markets/',
award: '$25,000', award: '$25,000',
endTime: toDate('Jul 31, 2023'), endTime: toDate('Jul 31, 2023'),
markets: [], contractIds: [],
images: [ images: [
{ {
marketUrl: marketUrl:
@ -107,33 +107,27 @@ const tourneys: Tourney[] = [
// }, // },
] ]
export async function getStaticProps() { type SectionInfo = {
const groupIds = tourneys tourney: Tourney
.map((data) => data.groupId) slug: string
.filter((id) => id != undefined) as string[] numPeople: number
const groups = (await Promise.all(groupIds.map(getGroup)))
// Then remove undefined groups
.filter(Boolean) as Group[]
const contracts = await Promise.all(
groups.map((g) => listContractsByGroupSlug(g?.slug ?? ''))
)
const markets = Object.fromEntries(groups.map((g, i) => [g.id, contracts[i]]))
const groupMap = keyBy(groups, 'id')
const numPeople = mapValues(groupMap, (g) => g?.totalMembers)
const slugs = mapValues(groupMap, 'slug')
return { props: { markets, numPeople, slugs }, revalidate: 60 * 10 }
} }
export default function TournamentPage(props: { export async function getStaticProps() {
markets: { [groupId: string]: Contract[] } const groupIds = tourneys.map((data) => data.groupId)
numPeople: { [groupId: string]: number } const groups = await Promise.all(groupIds.map(getGroup))
slugs: { [groupId: string]: string } const sections = zip(tourneys, groups)
}) { .filter(([_tourney, group]) => group != null)
const { markets = {}, numPeople = {}, slugs = {} } = props .map(([tourney, group]) => ({
tourney,
slug: group!.slug, // eslint-disable-line
numPeople: group!.totalMembers, // eslint-disable-line
}))
return { props: { sections } }
}
export default function TournamentPage(props: { sections: SectionInfo[] }) {
const { sections } = props
return ( return (
<Page> <Page>
@ -141,42 +135,44 @@ export default function TournamentPage(props: {
title="Tournaments" title="Tournaments"
description="Win money by betting in forecasting touraments on current events, sports, science, and more" description="Win money by betting in forecasting touraments on current events, sports, science, and more"
/> />
<Col className="mx-4 mt-4 gap-20 sm:mx-10 xl:w-[125%]"> <Col className="mx-4 mt-4 gap-10 sm:mx-10 xl:w-[125%]">
{tourneys.map(({ groupId, ...data }) => ( {sections.map(({ tourney, slug, numPeople }) => (
<Section <div key={slug}>
key={groupId} <SectionHeader
{...data} url={groupPath(slug)}
url={groupPath(slugs[groupId])} title={tourney.title}
ppl={numPeople[groupId] ?? 0} ppl={numPeople}
markets={markets[groupId] ?? []} award={tourney.award}
endTime={tourney.endTime}
/> />
<span>{tourney.blurb}</span>
<MarketCarousel slug={slug} />
</div>
))} ))}
<Section {...Salem} /> <div>
<SectionHeader
url={Salem.url}
title={Salem.title}
award={Salem.award}
endTime={Salem.endTime}
/>
<span>{Salem.blurb}</span>
<ImageCarousel url={Salem.url} images={Salem.images} />
</div>
</Col> </Col>
</Page> </Page>
) )
} }
function Section(props: { const SectionHeader = (props: {
title: string
url: string url: string
blurb: string title: string
award?: string
ppl?: number ppl?: number
endTime?: Dayjs award?: string
markets: Contract[] endTime?: number
images?: { marketUrl: string; image: StaticImageData }[] // hack for cspi }) => {
}) { const { url, title, ppl, award, endTime } = props
const { title, url, blurb, award, ppl, endTime, images } = props
// Sort markets by probability, highest % first
const markets = sortBy(props.markets, (c) =>
getProbability(c as BinaryContract | PseudoNumericContract)
)
.reverse()
.filter((c) => !c.isResolved)
return ( return (
<div>
<Link href={url}> <Link href={url}>
<a className="group mb-3 flex flex-wrap justify-between"> <a className="group mb-3 flex flex-wrap justify-between">
<h2 className="text-xl font-semibold group-hover:underline md:text-3xl"> <h2 className="text-xl font-semibold group-hover:underline md:text-3xl">
@ -191,33 +187,26 @@ function Section(props: {
</span> </span>
)} )}
{endTime && ( {endTime && (
<DateTimeTooltip time={endTime.valueOf()} text="Ends"> <DateTimeTooltip time={endTime} text="Ends">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<ClockIcon className="h-4" /> <ClockIcon className="h-4" />
{endTime.format('MMM D')} {dayjs(endTime).format('MMM D')}
</span> </span>
</DateTimeTooltip> </DateTimeTooltip>
)} )}
</Row> </Row>
</a> </a>
</Link> </Link>
<span>{blurb}</span> )
<Carousel className="-mx-4 mt-2 sm:-mx-10"> }
const ImageCarousel = (props: { images: MarketImage[]; url: string }) => {
const { images, url } = props
return (
<Carousel className="-mx-4 mt-4 sm:-mx-10">
<div className="shrink-0 sm:w-6" /> <div className="shrink-0 sm:w-6" />
{markets.length ? ( {images.map(({ marketUrl, image }) => (
markets.map((m) => ( <a key={marketUrl} href={marketUrl} className="hover:brightness-95">
<ContractCard
contract={m}
hideGroupLink
className="mb-2 max-h-[200px] w-96 shrink-0"
questionClass="line-clamp-3"
trackingPostfix=" tournament"
/>
))
) : (
<>
{images?.map(({ marketUrl, image }) => (
<a href={marketUrl} className="hover:brightness-95">
<NaturalImage src={image} /> <NaturalImage src={image} />
</a> </a>
))} ))}
@ -227,10 +216,30 @@ function Section(props: {
> >
See more See more
</SiteLink> </SiteLink>
</>
)}
</Carousel> </Carousel>
</div> )
}
const MarketCarousel = (props: { slug: string }) => {
const { slug } = props
const q = contractsByGroupSlugQuery(slug)
const { allItems, getNext, isLoading } = usePagination({ q, pageSize: 12 })
return isLoading ? (
<LoadingIndicator className="mt-10" />
) : (
<Carousel className="-mx-4 mt-4 sm:-mx-10" loadMore={getNext}>
<div className="shrink-0 sm:w-6" />
{allItems().map((m) => (
<ContractCard
key={m.id}
contract={m}
hideGroupLink
className="mb-2 max-h-[200px] w-96 shrink-0"
questionClass="line-clamp-3"
trackingPostfix=" tournament"
/>
))}
</Carousel>
) )
} }