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:
parent
0938368e30
commit
c0383bcf26
|
@ -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)}>
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,96 +135,111 @@ 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">
|
{title}
|
||||||
{title}
|
</h2>
|
||||||
</h2>
|
<Row className="my-2 items-center gap-4 whitespace-nowrap rounded-full bg-gray-200 px-6">
|
||||||
<Row className="my-2 items-center gap-4 whitespace-nowrap rounded-full bg-gray-200 px-6">
|
{!!award && <span className="flex items-center">🏆 {award}</span>}
|
||||||
{!!award && <span className="flex items-center">🏆 {award}</span>}
|
{!!ppl && (
|
||||||
{!!ppl && (
|
<span className="flex items-center gap-1">
|
||||||
|
<UsersIcon className="h-4" />
|
||||||
|
{ppl}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{endTime && (
|
||||||
|
<DateTimeTooltip time={endTime} text="Ends">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<UsersIcon className="h-4" />
|
<ClockIcon className="h-4" />
|
||||||
{ppl}
|
{dayjs(endTime).format('MMM D')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
</DateTimeTooltip>
|
||||||
{endTime && (
|
)}
|
||||||
<DateTimeTooltip time={endTime.valueOf()} text="Ends">
|
</Row>
|
||||||
<span className="flex items-center gap-1">
|
</a>
|
||||||
<ClockIcon className="h-4" />
|
</Link>
|
||||||
{endTime.format('MMM D')}
|
)
|
||||||
</span>
|
}
|
||||||
</DateTimeTooltip>
|
|
||||||
)}
|
const ImageCarousel = (props: { images: MarketImage[]; url: string }) => {
|
||||||
</Row>
|
const { images, url } = props
|
||||||
|
return (
|
||||||
|
<Carousel className="-mx-4 mt-4 sm:-mx-10">
|
||||||
|
<div className="shrink-0 sm:w-6" />
|
||||||
|
{images.map(({ marketUrl, image }) => (
|
||||||
|
<a key={marketUrl} href={marketUrl} className="hover:brightness-95">
|
||||||
|
<NaturalImage src={image} />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
))}
|
||||||
<span>{blurb}</span>
|
<SiteLink
|
||||||
<Carousel className="-mx-4 mt-2 sm:-mx-10">
|
className="ml-6 mr-10 flex shrink-0 items-center text-indigo-700"
|
||||||
<div className="shrink-0 sm:w-6" />
|
href={url}
|
||||||
{markets.length ? (
|
>
|
||||||
markets.map((m) => (
|
See more
|
||||||
<ContractCard
|
</SiteLink>
|
||||||
contract={m}
|
</Carousel>
|
||||||
hideGroupLink
|
)
|
||||||
className="mb-2 max-h-[200px] w-96 shrink-0"
|
}
|
||||||
questionClass="line-clamp-3"
|
|
||||||
trackingPostfix=" tournament"
|
const MarketCarousel = (props: { slug: string }) => {
|
||||||
/>
|
const { slug } = props
|
||||||
))
|
const q = contractsByGroupSlugQuery(slug)
|
||||||
) : (
|
const { allItems, getNext, isLoading } = usePagination({ q, pageSize: 12 })
|
||||||
<>
|
return isLoading ? (
|
||||||
{images?.map(({ marketUrl, image }) => (
|
<LoadingIndicator className="mt-10" />
|
||||||
<a href={marketUrl} className="hover:brightness-95">
|
) : (
|
||||||
<NaturalImage src={image} />
|
<Carousel className="-mx-4 mt-4 sm:-mx-10" loadMore={getNext}>
|
||||||
</a>
|
<div className="shrink-0 sm:w-6" />
|
||||||
))}
|
{allItems().map((m) => (
|
||||||
<SiteLink
|
<ContractCard
|
||||||
className="ml-6 mr-10 flex shrink-0 items-center text-indigo-700"
|
key={m.id}
|
||||||
href={url}
|
contract={m}
|
||||||
>
|
hideGroupLink
|
||||||
See more
|
className="mb-2 max-h-[200px] w-96 shrink-0"
|
||||||
</SiteLink>
|
questionClass="line-clamp-3"
|
||||||
</>
|
trackingPostfix=" tournament"
|
||||||
)}
|
/>
|
||||||
</Carousel>
|
))}
|
||||||
</div>
|
</Carousel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user