325 lines
9.8 KiB
TypeScript
325 lines
9.8 KiB
TypeScript
import clsx from 'clsx'
|
|
import React from 'react'
|
|
import { formatMoney } from 'common/util/format'
|
|
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 { Title } from 'web/components/title'
|
|
import { useUser } from 'web/hooks/use-user'
|
|
import { fromNow } from 'web/lib/util/time'
|
|
|
|
import dayjs from 'dayjs'
|
|
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
|
import {
|
|
getChallengeUrl,
|
|
useAcceptedChallenges,
|
|
useUserChallenges,
|
|
} from 'web/lib/firebase/challenges'
|
|
import { Challenge, CHALLENGES_ENABLED } from 'common/challenge'
|
|
import { Tabs } from 'web/components/layout/tabs'
|
|
import { SiteLink } from 'web/components/site-link'
|
|
import { Avatar } from 'web/components/avatar'
|
|
import Router from 'next/router'
|
|
import { contractPathWithoutContract } from 'web/lib/firebase/contracts'
|
|
import { Button } from 'web/components/button'
|
|
import { ClipboardCopyIcon, QrcodeIcon } from '@heroicons/react/outline'
|
|
import { copyToClipboard } from 'web/lib/util/copy'
|
|
import toast from 'react-hot-toast'
|
|
import { Modal } from 'web/components/layout/modal'
|
|
import { QRCode } from 'web/components/qr-code'
|
|
import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal'
|
|
import { UserLink } from 'web/components/user-link'
|
|
|
|
dayjs.extend(customParseFormat)
|
|
const columnClass = 'sm:px-5 px-2 py-3.5 max-w-[100px] truncate'
|
|
const amountClass = columnClass + ' max-w-[75px] font-bold'
|
|
|
|
export default function ChallengesListPage() {
|
|
const user = useUser()
|
|
const challenges = useAcceptedChallenges()
|
|
const [open, setOpen] = React.useState(false)
|
|
const userChallenges = useUserChallenges(user?.id)
|
|
.concat(
|
|
user ? challenges.filter((c) => c.acceptances[0].userId === user.id) : []
|
|
)
|
|
.sort((a, b) => b.createdTime - a.createdTime)
|
|
|
|
const userTab = user
|
|
? [
|
|
{
|
|
content: <YourChallengesTable links={userChallenges} />,
|
|
title: 'Your Challenges',
|
|
},
|
|
]
|
|
: []
|
|
|
|
const publicTab = [
|
|
{
|
|
content: <PublicChallengesTable links={challenges} />,
|
|
title: 'Public Challenges',
|
|
},
|
|
]
|
|
|
|
return (
|
|
<Page>
|
|
<SEO
|
|
title="Challenges"
|
|
description="Challenge your friends to a bet!"
|
|
url="/send"
|
|
/>
|
|
|
|
<Col className="w-full px-8">
|
|
<Row className="items-center justify-between">
|
|
<Title text="Challenges" />
|
|
{CHALLENGES_ENABLED && (
|
|
<Button size="lg" color="gradient" onClick={() => setOpen(true)}>
|
|
Create Challenge
|
|
<CreateChallengeModal
|
|
isOpen={open}
|
|
setOpen={setOpen}
|
|
user={user}
|
|
/>
|
|
</Button>
|
|
)}
|
|
</Row>
|
|
<p>
|
|
Want to create your own challenge?
|
|
<SiteLink className={'mx-1 font-bold'} href={'/home'}>
|
|
Find
|
|
</SiteLink>
|
|
a market you and a friend disagree on and hit the challenge button, or
|
|
tap the button above to create a new market & challenge in one.
|
|
</p>
|
|
|
|
<Tabs className="mb-4" tabs={[...userTab, ...publicTab]} />
|
|
</Col>
|
|
</Page>
|
|
)
|
|
}
|
|
|
|
function YourChallengesTable(props: { links: Challenge[] }) {
|
|
const { links } = props
|
|
return links.length == 0 ? (
|
|
<p>There aren't currently any challenges.</p>
|
|
) : (
|
|
<div className="overflow-scroll">
|
|
<table className="w-full divide-y divide-gray-300 rounded-lg border border-gray-200">
|
|
<thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900">
|
|
<tr>
|
|
<th className={amountClass}>Amount</th>
|
|
<th
|
|
className={clsx(
|
|
columnClass,
|
|
'text-center sm:pl-10 sm:text-start'
|
|
)}
|
|
>
|
|
Link
|
|
</th>
|
|
<th className={columnClass}>Accepted By</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className={'divide-y divide-gray-200 bg-white'}>
|
|
{links.map((link) => (
|
|
<YourLinkSummaryRow challenge={link} />
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function YourLinkSummaryRow(props: { challenge: Challenge }) {
|
|
const { challenge } = props
|
|
const { acceptances } = challenge
|
|
|
|
const [open, setOpen] = React.useState(false)
|
|
const className = clsx(
|
|
'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white'
|
|
)
|
|
return (
|
|
<>
|
|
<Modal open={open} setOpen={setOpen} size={'sm'}>
|
|
<Col
|
|
className={
|
|
'items-center justify-center gap-4 rounded-md bg-white p-8 py-8 '
|
|
}
|
|
>
|
|
<span className={'mb-4 text-center text-xl text-indigo-700'}>
|
|
Have your friend scan this to accept the challenge!
|
|
</span>
|
|
<QRCode url={getChallengeUrl(challenge)} />
|
|
</Col>
|
|
</Modal>
|
|
<tr id={challenge.slug} key={challenge.slug} className={className}>
|
|
<td className={amountClass}>
|
|
<SiteLink href={getChallengeUrl(challenge)}>
|
|
{formatMoney(challenge.creatorAmount)}
|
|
</SiteLink>
|
|
</td>
|
|
<td
|
|
className={clsx(
|
|
columnClass,
|
|
'text-center sm:max-w-[200px] sm:text-start'
|
|
)}
|
|
>
|
|
<Row className="items-center gap-2">
|
|
<Button
|
|
color="gray-white"
|
|
size="xs"
|
|
onClick={() => {
|
|
copyToClipboard(getChallengeUrl(challenge))
|
|
toast('Link copied to clipboard!')
|
|
}}
|
|
>
|
|
<ClipboardCopyIcon className={'h-5 w-5 sm:h-4 sm:w-4'} />
|
|
</Button>
|
|
<Button
|
|
color="gray-white"
|
|
size="xs"
|
|
onClick={() => {
|
|
setOpen(true)
|
|
}}
|
|
>
|
|
<QrcodeIcon className="h-5 w-5 sm:h-4 sm:w-4" />
|
|
</Button>
|
|
<SiteLink
|
|
href={getChallengeUrl(challenge)}
|
|
className={'mx-1 mb-1 hidden sm:inline-block'}
|
|
>
|
|
{`...${challenge.contractSlug}/${challenge.slug}`}
|
|
</SiteLink>
|
|
</Row>
|
|
</td>
|
|
|
|
<td className={columnClass}>
|
|
<Row className={'items-center justify-start gap-1'}>
|
|
{acceptances.length > 0 ? (
|
|
<>
|
|
<Avatar
|
|
username={acceptances[0].userUsername}
|
|
avatarUrl={acceptances[0].userAvatarUrl}
|
|
size={'sm'}
|
|
/>
|
|
<UserLink
|
|
name={acceptances[0].userName}
|
|
username={acceptances[0].userUsername}
|
|
/>
|
|
</>
|
|
) : (
|
|
<span>
|
|
No one -
|
|
{challenge.expiresTime &&
|
|
` (expires ${fromNow(challenge.expiresTime)})`}
|
|
</span>
|
|
)}
|
|
</Row>
|
|
</td>
|
|
</tr>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function PublicChallengesTable(props: { links: Challenge[] }) {
|
|
const { links } = props
|
|
return links.length == 0 ? (
|
|
<p>There aren't currently any challenges.</p>
|
|
) : (
|
|
<div className="overflow-scroll">
|
|
<table className="w-full divide-y divide-gray-300 rounded-lg border border-gray-200">
|
|
<thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900">
|
|
<tr>
|
|
<th className={amountClass}>Amount</th>
|
|
<th className={columnClass}>Creator</th>
|
|
<th className={columnClass}>Acceptor</th>
|
|
<th className={columnClass}>Market</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className={'divide-y divide-gray-200 bg-white'}>
|
|
{links.map((link) => (
|
|
<PublicLinkSummaryRow challenge={link} />
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function PublicLinkSummaryRow(props: { challenge: Challenge }) {
|
|
const { challenge } = props
|
|
const {
|
|
acceptances,
|
|
creatorUsername,
|
|
creatorName,
|
|
creatorAvatarUrl,
|
|
contractCreatorUsername,
|
|
contractQuestion,
|
|
contractSlug,
|
|
} = challenge
|
|
|
|
const className = clsx(
|
|
'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white'
|
|
)
|
|
return (
|
|
<tr
|
|
id={challenge.slug + '-public'}
|
|
key={challenge.slug + '-public'}
|
|
className={className}
|
|
onClick={() => Router.push(getChallengeUrl(challenge))}
|
|
>
|
|
<td className={amountClass}>
|
|
<SiteLink href={getChallengeUrl(challenge)}>
|
|
{formatMoney(challenge.creatorAmount)}
|
|
</SiteLink>
|
|
</td>
|
|
|
|
<td className={clsx(columnClass)}>
|
|
<Row className={'items-center justify-start gap-1'}>
|
|
<Avatar
|
|
username={creatorUsername}
|
|
avatarUrl={creatorAvatarUrl}
|
|
size={'sm'}
|
|
noLink={true}
|
|
/>
|
|
<UserLink name={creatorName} username={creatorUsername} />
|
|
</Row>
|
|
</td>
|
|
|
|
<td className={clsx(columnClass)}>
|
|
<Row className={'items-center justify-start gap-1'}>
|
|
{acceptances.length > 0 ? (
|
|
<>
|
|
<Avatar
|
|
username={acceptances[0].userUsername}
|
|
avatarUrl={acceptances[0].userAvatarUrl}
|
|
size={'sm'}
|
|
noLink={true}
|
|
/>
|
|
<UserLink
|
|
name={acceptances[0].userName}
|
|
username={acceptances[0].userUsername}
|
|
/>
|
|
</>
|
|
) : (
|
|
<span>
|
|
No one -
|
|
{challenge.expiresTime &&
|
|
` (expires ${fromNow(challenge.expiresTime)})`}
|
|
</span>
|
|
)}
|
|
</Row>
|
|
</td>
|
|
<td className={clsx(columnClass, 'font-bold')}>
|
|
<SiteLink
|
|
href={contractPathWithoutContract(
|
|
contractCreatorUsername,
|
|
contractSlug
|
|
)}
|
|
>
|
|
{contractQuestion}
|
|
</SiteLink>
|
|
</td>
|
|
</tr>
|
|
)
|
|
}
|