manifold/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx
2022-08-04 16:09:33 -06:00

404 lines
12 KiB
TypeScript

import React, { useEffect, useState } from 'react'
import Confetti from 'react-confetti'
import { fromPropz, usePropz } from 'web/hooks/use-propz'
import { contractPath, getContractFromSlug } from 'web/lib/firebase/contracts'
import { useContractWithPreload } from 'web/hooks/use-contract'
import { DOMAIN } from 'common/envs/constants'
import { Col } from 'web/components/layout/col'
import { SiteLink } from 'web/components/site-link'
import { Spacer } from 'web/components/layout/spacer'
import { Row } from 'web/components/layout/row'
import { Challenge } from 'common/challenge'
import {
getChallenge,
getChallengeUrl,
useChallenge,
} from 'web/lib/firebase/challenges'
import { getUserByUsername } from 'web/lib/firebase/users'
import { User } from 'common/user'
import { Page } from 'web/components/page'
import { useUser, useUserById } from 'web/hooks/use-user'
import { AcceptChallengeButton } from 'web/components/challenges/accept-challenge-button'
import { Avatar } from 'web/components/avatar'
import { UserLink } from 'web/components/user-page'
import { BinaryOutcomeLabel } from 'web/components/outcome-label'
import { formatMoney } from 'common/util/format'
import { LoadingIndicator } from 'web/components/loading-indicator'
import { useWindowSize } from 'web/hooks/use-window-size'
import { Bet, listAllBets } from 'web/lib/firebase/bets'
import { SEO } from 'web/components/SEO'
import { getOpenGraphProps } from 'web/components/contract/contract-card-preview'
import Custom404 from 'web/pages/404'
import { useSaveReferral } from 'web/hooks/use-save-referral'
import { BinaryContract } from 'common/contract'
import { Title } from 'web/components/title'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: {
params: { username: string; contractSlug: string; challengeSlug: string }
}) {
const { username, contractSlug, challengeSlug } = props.params
const contract = (await getContractFromSlug(contractSlug)) || null
const user = (await getUserByUsername(username)) || null
const bets = contract?.id ? await listAllBets(contract.id) : []
const challenge = contract?.id
? await getChallenge(challengeSlug, contract.id)
: null
return {
props: {
contract,
user,
slug: contractSlug,
challengeSlug,
bets,
challenge,
},
revalidate: 60, // regenerate after a minute
}
}
export async function getStaticPaths() {
return { paths: [], fallback: 'blocking' }
}
export default function ChallengePage(props: {
contract: BinaryContract | null
user: User
slug: string
bets: Bet[]
challenge: Challenge | null
challengeSlug: string
}) {
props = usePropz(props, getStaticPropz) ?? {
contract: null,
user: null,
challengeSlug: '',
bets: [],
challenge: null,
slug: '',
}
const contract = (useContractWithPreload(props.contract) ??
props.contract) as BinaryContract
const challenge =
useChallenge(props.challengeSlug, contract?.id) ?? props.challenge
const { user, bets } = props
const currentUser = useUser()
useSaveReferral(currentUser, {
defaultReferrerUsername: challenge?.creatorUsername,
})
if (!contract || !challenge) return <Custom404 />
const ogCardProps = getOpenGraphProps(contract)
ogCardProps.creatorUsername = challenge.creatorUsername
ogCardProps.creatorName = challenge.creatorName
ogCardProps.creatorAvatarUrl = challenge.creatorAvatarUrl
return (
<Page>
<SEO
title={ogCardProps.question}
description={ogCardProps.description}
url={getChallengeUrl(challenge).replace('https://', '')}
ogCardProps={ogCardProps}
challenge={challenge}
/>
{challenge.acceptances.length >= challenge.maxUses ? (
<ClosedChallengeContent
contract={contract}
challenge={challenge}
creator={user}
/>
) : (
<OpenChallengeContent
user={currentUser}
contract={contract}
challenge={challenge}
creator={user}
bets={bets}
/>
)}
<FAQ />
</Page>
)
}
function FAQ() {
const [toggleWhatIsThis, setToggleWhatIsThis] = useState(false)
const [toggleWhatIsMana, setToggleWhatIsMana] = useState(false)
return (
<Col className={'items-center gap-4 p-2 md:p-6 lg:items-start'}>
<Row className={'text-xl text-indigo-700'}>FAQ</Row>
<Row className={'text-lg text-indigo-700'}>
<span
className={'mx-2 cursor-pointer'}
onClick={() => setToggleWhatIsThis(!toggleWhatIsThis)}
>
{toggleWhatIsThis ? '-' : '+'}
What is this?
</span>
</Row>
{toggleWhatIsThis && (
<Row className={'mx-4'}>
<span>
This is a challenge bet, or a bet offered from one person to another
that is only realized if both parties agree. You can agree to the
challenge (if it's open) or create your own from a market page. See
more markets{' '}
<SiteLink className={'font-bold'} href={'/home'}>
here.
</SiteLink>
</span>
</Row>
)}
<Row className={'text-lg text-indigo-700'}>
<span
className={'mx-2 cursor-pointer'}
onClick={() => setToggleWhatIsMana(!toggleWhatIsMana)}
>
{toggleWhatIsMana ? '-' : '+'}
What is M$?
</span>
</Row>
{toggleWhatIsMana && (
<Row className={'mx-4'}>
Mana (M$) is the play-money used by our platform to keep track of your
bets. It's completely free for you and your friends to get started!
</Row>
)}
</Col>
)
}
function ClosedChallengeContent(props: {
contract: BinaryContract
challenge: Challenge
creator: User
}) {
const { contract, challenge, creator } = props
const { resolution, question } = contract
const {
acceptances,
creatorAmount,
creatorOutcome,
acceptorOutcome,
acceptorAmount,
} = challenge
const user = useUserById(acceptances[0].userId)
const [showConfetti, setShowConfetti] = useState(false)
const { width, height } = useWindowSize()
useEffect(() => {
if (acceptances.length === 0) return
if (acceptances[0].createdTime > Date.now() - 1000 * 60)
setShowConfetti(true)
}, [acceptances])
const creatorWon = resolution === creatorOutcome
const href = `https://${DOMAIN}${contractPath(contract)}`
if (!user) return <LoadingIndicator />
const winner = (creatorWon ? creator : user).name
return (
<>
{showConfetti && (
<Confetti
width={width ?? 500}
height={height ?? 500}
confettiSource={{
x: ((width ?? 500) - 200) / 2,
y: 0,
w: 200,
h: 0,
}}
recycle={false}
initialVelocityY={{ min: 1, max: 3 }}
numberOfPieces={200}
/>
)}
<Col className=" w-full items-center justify-center rounded border-0 border-gray-100 bg-white py-6 pl-1 pr-2 sm:px-2 md:px-6 md:py-8 ">
{resolution ? (
<>
<Title className="!mt-0" text={`🥇 ${winner} wins the bet 🥇`} />
<SiteLink href={href} className={'mb-8 text-xl'}>
{question}
</SiteLink>
</>
) : (
<SiteLink href={href} className={'mb-8'}>
<span className="text-3xl text-indigo-700">{question}</span>
</SiteLink>
)}
<Col
className={'w-full content-between justify-between gap-1 sm:flex-row'}
>
<UserBetColumn
challenger={creator}
outcome={creatorOutcome}
amount={creatorAmount}
isResolved={!!resolution}
/>
<Col className="items-center justify-center py-8 text-2xl sm:text-4xl">
VS
</Col>
<UserBetColumn
challenger={user?.id === creator.id ? undefined : user}
outcome={acceptorOutcome}
amount={acceptorAmount}
isResolved={!!resolution}
/>
</Col>
<Spacer h={3} />
{/* <Row className="mt-8 items-center">
<span className='mr-4'>Share</span> <CopyLinkButton url={window.location.href} />
</Row> */}
</Col>
</>
)
}
function OpenChallengeContent(props: {
contract: BinaryContract
challenge: Challenge
creator: User
user: User | null | undefined
bets: Bet[]
}) {
const { contract, challenge, creator, user } = props
const { question } = contract
const {
creatorAmount,
creatorId,
creatorOutcome,
acceptorAmount,
acceptorOutcome,
} = challenge
const href = `https://${DOMAIN}${contractPath(contract)}`
return (
<Col className="items-center">
<Col className="h-full items-center justify-center rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
<SiteLink href={href} className={'mb-8'}>
<span className="text-3xl text-indigo-700">{question}</span>
</SiteLink>
<Col
className={
' w-full content-between justify-between gap-1 sm:flex-row'
}
>
<UserBetColumn
challenger={creator}
outcome={creatorOutcome}
amount={creatorAmount}
/>
<Col className="items-center justify-center py-4 text-2xl sm:py-8 sm:text-4xl">
VS
</Col>
<UserBetColumn
challenger={user?.id === creatorId ? undefined : user}
outcome={acceptorOutcome}
amount={acceptorAmount}
/>
</Col>
<Spacer h={3} />
<Row className={'my-4 text-center text-gray-500'}>
<span>
{`${creator.name} will bet ${formatMoney(
creatorAmount
)} on ${creatorOutcome} if you bet ${formatMoney(
acceptorAmount
)} on ${acceptorOutcome}. Whoever is right will get `}
<span className="mr-1 font-bold ">
{formatMoney(creatorAmount + acceptorAmount)}
</span>
total.
</span>
</Row>
<Row className="my-4 w-full items-center justify-center">
<AcceptChallengeButton
user={user}
contract={contract}
challenge={challenge}
/>
</Row>
</Col>
</Col>
)
}
const userCol = (challenger: User) => (
<Col className={'mb-2 w-full items-center justify-center gap-2'}>
<UserLink
className={'text-2xl'}
name={challenger.name}
username={challenger.username}
/>
<Avatar
size={24}
avatarUrl={challenger.avatarUrl}
username={challenger.username}
/>
</Col>
)
function UserBetColumn(props: {
challenger: User | null | undefined
outcome: string
amount: number
isResolved?: boolean
}) {
const { challenger, outcome, amount, isResolved } = props
return (
<Col className="w-full items-start justify-center gap-1">
{challenger ? (
userCol(challenger)
) : (
<Col className={'mb-2 w-full items-center justify-center gap-2'}>
<span className={'text-2xl'}>You</span>
<Avatar
className={'h-[7.25rem] w-[7.25rem]'}
avatarUrl={undefined}
username={undefined}
/>
</Col>
)}
<Row className={'w-full items-center justify-center'}>
<span className={'text-lg'}>
{isResolved ? 'had bet' : challenger ? '' : ''}
</span>
</Row>
<Row className={'w-full items-center justify-center'}>
<span className={'text-lg'}>
<span className="bold text-2xl">{formatMoney(amount)}</span>
{' on '}
<span className="bold text-2xl">
<BinaryOutcomeLabel outcome={outcome as any} />
</span>{' '}
</span>
</Row>
</Col>
)
}