manifold/web/pages/tournament/index.tsx
2022-08-24 18:02:01 -07:00

210 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ClockIcon } from '@heroicons/react/outline'
import {
ChevronLeftIcon,
ChevronRightIcon,
UsersIcon,
} from '@heroicons/react/solid'
import clsx from 'clsx'
import { Contract } from 'common/contract'
import { formatLargeNumber } from 'common/util/format'
import dayjs, { Dayjs } from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import { throttle } from 'lodash'
import { ReactNode, useEffect, useRef, useState } from 'react'
import { ContractCard } from 'web/components/contract/contract-card'
import { DateTimeTooltip } from 'web/components/datetime-tooltip'
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 { useContracts } from 'web/hooks/use-contracts'
import { useGroup } from 'web/hooks/use-group'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
const toDate = (d: string) => dayjs(d, 'MMM D, YYYY').tz('America/Los_Angeles')
type Tourney = {
title: string
url?: string
blurb: string // actual description in the click-through
award?: number
endTime?: Dayjs
// TODO: somehow get the markets
groupId?: string
}
const tourneys: Tourney[] = [
{
title: 'CSPI/Salem Tournament',
blurb:
'Forecasting contest - top 5 can become Salem Centers next research fellow.',
url: 'https://salemcenter.manifold.markets/',
award: 25_000,
},
{
title: 'Fantasy Football Stock Exchange',
blurb: 'How many points will each NFL player score?',
url: 'https://manifold.markets/group/fantasy-football-stock-exchange',
award: 500,
endTime: toDate('Jan 6, 2023'),
groupId: 'SxGRqXRpV3RAQKudbcNb',
},
{
title: 'Cause Exploration Prize',
blurb:
'Which new charity ideas will Open Philanthropy find most promising?',
award: 100_000,
endTime: toDate('Sep 9, 2022'),
},
{
title: 'Clearer Thinking Regrant Project',
blurb: 'Something amazing',
award: 1_000_000,
endTime: toDate('Sep 22, 2022'),
},
]
export default function TournamentPage() {
const ffsx = useGroup('SxGRqXRpV3RAQKudbcNb')
const markets = useContracts() ?? []
const ffsxMarkets = markets
.filter((m) => (ffsx?.contractIds ?? []).includes(m.id))
.slice(0, 50)
const ffsxLength = ffsx?.memberIds.length
useEffect(() => console.log(tourneys), [])
return (
<Page>
<SEO
title="Tournaments"
description="Win money by betting in forecasting touraments on current events, sports, science, and more"
/>
<Col className="mx-4 sm:mx-10 xl:w-[125%]">
<h1 className="my-4 text-2xl text-indigo-700">Tournaments</h1>
{tourneys.map(({ groupId, ...data }) => (
<Section {...data} markets={groupId ? ffsxMarkets : []} />
))}
</Col>
</Page>
)
}
function Section(props: {
title: string
url?: string
blurb: string
award?: number
endTime?: Dayjs
markets: Contract[]
}) {
const { title, url, blurb, award, endTime, markets } = props
return (
<div className="my-4">
<Row className="mb-3 flex-wrap justify-between">
<h2 className="text-xl font-semibold hover:underline">
{url ? <a href={url}>{title}</a> : title}
</h2>
<Row className="items-center gap-4 whitespace-nowrap rounded-full bg-gray-200 px-6">
{!!award && (
<span className="flex items-center">
🏆 ${formatLargeNumber(award)}
</span>
)}
<span className="flex items-center gap-1">
<UsersIcon className="h-4" />
400
</span>
{endTime && (
<DateTimeTooltip time={endTime} text="Ends">
<span className="flex items-center gap-1">
<ClockIcon className="h-4" />
{endTime.format('MMM D')}
</span>
</DateTimeTooltip>
)}
</Row>
</Row>
<span>{blurb}</span>
<Carousel className="-mx-4 mt-2 sm:-mx-10">
<div className="shrink-0 sm:w-6" />
{markets.length ? (
markets.map((m) => (
<ContractCard
contract={m}
showHotVolume
hideGroupLink
className="shrink-0"
/>
))
) : (
<div className="flex h-32 w-80 items-center justify-center rounded bg-white text-lg text-gray-700 shadow-md">
Coming Soon...
</div>
)}
</Carousel>
</div>
)
}
function Carousel(props: { children: ReactNode; className?: string }) {
const { children, className } = props
const ref = useRef<HTMLDivElement>(null)
const th = (f: () => any) => throttle(f, 500, { trailing: false })
const scrollLeft = th(() =>
ref.current?.scrollBy({ left: -ref.current.clientWidth })
)
const scrollRight = th(() =>
ref.current?.scrollBy({ left: ref.current.clientWidth })
)
const [atFront, setAtFront] = useState(true)
const [atBack, setAtBack] = useState(false)
const onScroll = throttle(() => {
if (ref.current) {
const { scrollLeft, clientWidth, scrollWidth } = ref.current
setAtFront(scrollLeft < 80)
setAtBack(scrollWidth - (clientWidth + scrollLeft) < 80)
}
}, 500)
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(onScroll, [])
return (
<div className={clsx('relative', className)}>
<Row
className="scrollbar-hide w-full gap-4 overflow-x-auto scroll-smooth"
ref={ref}
onScroll={onScroll}
>
{children}
</Row>
{!atFront && (
<div
className="absolute left-0 top-0 bottom-0 z-10 flex w-10 cursor-pointer items-center justify-center hover:bg-indigo-100/30"
onMouseDown={scrollLeft}
>
<ChevronLeftIcon className="h-7 w-7 rounded-full bg-indigo-50 text-indigo-700" />
</div>
)}
{!atBack && (
<div
className="absolute right-0 top-0 bottom-0 z-10 flex w-10 cursor-pointer items-center justify-center hover:bg-indigo-100/30"
onMouseDown={scrollRight}
>
<ChevronRightIcon className="h-7 w-7 rounded-full bg-indigo-50 text-indigo-700" />
</div>
)}
</div>
)
}