From c0383bcf26832fbd2498358f457d69ee3cfaae67 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Sat, 3 Sep 2022 09:55:10 -0700 Subject: [PATCH] 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 --- web/components/carousel.tsx | 2 +- web/hooks/use-pagination.ts | 1 + web/lib/firebase/contracts.ts | 11 +- web/pages/tournaments/index.tsx | 243 +++++++++++++++++--------------- 4 files changed, 137 insertions(+), 120 deletions(-) diff --git a/web/components/carousel.tsx b/web/components/carousel.tsx index 9719ba06..79baa451 100644 --- a/web/components/carousel.tsx +++ b/web/components/carousel.tsx @@ -33,7 +33,7 @@ export function Carousel(props: { }, 500) // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(onScroll, []) + useEffect(onScroll, [children]) return (
diff --git a/web/hooks/use-pagination.ts b/web/hooks/use-pagination.ts index 485afca8..ab991d1f 100644 --- a/web/hooks/use-pagination.ts +++ b/web/hooks/use-pagination.ts @@ -103,6 +103,7 @@ export const usePagination = (opts: PaginationOptions) => { isEnd: state.isComplete && state.pageEnd >= state.docs.length, getPrev: () => dispatch({ type: 'PREV' }), getNext: () => dispatch({ type: 'NEXT' }), + allItems: () => state.docs.map((d) => d.data()), getItems: () => state.docs.slice(state.pageStart, state.pageEnd).map((d) => d.data()), } diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index c7e32f71..5c65b23f 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -104,11 +104,18 @@ export async function listContracts(creatorId: string): Promise { 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( slug: string ): Promise { - const q = query(contracts, where('groupSlugs', 'array-contains', slug)) - const snapshot = await getDocs(q) + const snapshot = await getDocs(contractsByGroupSlugQuery(slug)) return snapshot.docs.map((doc) => doc.data()) } diff --git a/web/pages/tournaments/index.tsx b/web/pages/tournaments/index.tsx index 9bfdfb89..c9827f72 100644 --- a/web/pages/tournaments/index.tsx +++ b/web/pages/tournaments/index.tsx @@ -1,16 +1,10 @@ import { ClockIcon } from '@heroicons/react/outline' import { UsersIcon } from '@heroicons/react/solid' -import { - BinaryContract, - Contract, - PseudoNumericContract, -} from 'common/contract' -import { Group } from 'common/group' -import dayjs, { Dayjs } from 'dayjs' +import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' -import { keyBy, mapValues, sortBy } from 'lodash' +import { zip } from 'lodash' import Image, { ImageProps, StaticImageData } from 'next/image' import Link from 'next/link' import { useState } from 'react' @@ -20,27 +14,33 @@ import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' 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 elon_pic from './_cspi/Will_Elon_Buy_Twitter.png' import china_pic from './_cspi/Chinese_Military_Action_against_Taiwan.png' import mpox_pic from './_cspi/Monkeypox_Cases.png' import race_pic from './_cspi/Supreme_Court_Ban_Race_in_College_Admissions.png' import { SiteLink } from 'web/components/site-link' -import { getProbability } from 'common/calculate' 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(timezone) 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 = { title: string - url?: string blurb: string // actual description in the click-through award?: string - endTime?: Dayjs + endTime?: number groupId: string } @@ -50,7 +50,7 @@ const Salem = { url: 'https://salemcenter.manifold.markets/', award: '$25,000', endTime: toDate('Jul 31, 2023'), - markets: [], + contractIds: [], images: [ { marketUrl: @@ -107,33 +107,27 @@ const tourneys: Tourney[] = [ // }, ] -export async function getStaticProps() { - const groupIds = tourneys - .map((data) => data.groupId) - .filter((id) => id != undefined) as string[] - 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 } +type SectionInfo = { + tourney: Tourney + slug: string + numPeople: number } -export default function TournamentPage(props: { - markets: { [groupId: string]: Contract[] } - numPeople: { [groupId: string]: number } - slugs: { [groupId: string]: string } -}) { - const { markets = {}, numPeople = {}, slugs = {} } = props +export async function getStaticProps() { + const groupIds = tourneys.map((data) => data.groupId) + const groups = await Promise.all(groupIds.map(getGroup)) + const sections = zip(tourneys, groups) + .filter(([_tourney, group]) => group != null) + .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 ( @@ -141,96 +135,111 @@ export default function TournamentPage(props: { title="Tournaments" description="Win money by betting in forecasting touraments on current events, sports, science, and more" /> - - {tourneys.map(({ groupId, ...data }) => ( -
+ + {sections.map(({ tourney, slug, numPeople }) => ( +
+ + {tourney.blurb} + +
))} -
+
+ + {Salem.blurb} + +
) } -function Section(props: { - title: string +const SectionHeader = (props: { url: string - blurb: string - award?: string + title: string ppl?: number - endTime?: Dayjs - markets: Contract[] - images?: { marketUrl: string; image: StaticImageData }[] // hack for cspi -}) { - 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) - + award?: string + endTime?: number +}) => { + const { url, title, ppl, award, endTime } = props return ( -
- - -

- {title} -

- - {!!award && 🏆 {award}} - {!!ppl && ( + +
+

+ {title} +

+ + {!!award && 🏆 {award}} + {!!ppl && ( + + + {ppl} + + )} + {endTime && ( + - - {ppl} + + {dayjs(endTime).format('MMM D')} - )} - {endTime && ( - - - - {endTime.format('MMM D')} - - - )} - + + )} + +
+ + ) +} + +const ImageCarousel = (props: { images: MarketImage[]; url: string }) => { + const { images, url } = props + return ( + +
+ {images.map(({ marketUrl, image }) => ( + + - - {blurb} - -
- {markets.length ? ( - markets.map((m) => ( - - )) - ) : ( - <> - {images?.map(({ marketUrl, image }) => ( - - - - ))} - - See more - - - )} - -
+ ))} + + See more + +
+ ) +} + +const MarketCarousel = (props: { slug: string }) => { + const { slug } = props + const q = contractsByGroupSlugQuery(slug) + const { allItems, getNext, isLoading } = usePagination({ q, pageSize: 12 }) + return isLoading ? ( + + ) : ( + +
+ {allItems().map((m) => ( + + ))} + ) }