Restyle challenges list, cache contract name
This commit is contained in:
parent
5d0d2d0b46
commit
7cc4710ab1
|
@ -27,6 +27,8 @@ export type Challenge = {
|
|||
|
||||
contractId: string
|
||||
contractSlug: string
|
||||
contractQuestion: string
|
||||
contractCreatorUsername: string
|
||||
|
||||
createdTime: number
|
||||
// If null, the link is valid forever
|
||||
|
|
|
@ -65,6 +65,8 @@ export async function createChallenge(data: {
|
|||
acceptorAmount,
|
||||
contractSlug: contract.slug,
|
||||
contractId: contract.id,
|
||||
contractQuestion: contract.question,
|
||||
contractCreatorUsername: contract.creatorUsername,
|
||||
createdTime: Date.now(),
|
||||
expiresTime,
|
||||
maxUses: 1,
|
||||
|
|
|
@ -35,6 +35,13 @@ export function contractPath(contract: Contract) {
|
|||
return `/${contract.creatorUsername}/${contract.slug}`
|
||||
}
|
||||
|
||||
export function contractPathWithoutContract(
|
||||
creatorUsername: string,
|
||||
slug: string
|
||||
) {
|
||||
return `/${creatorUsername}/${slug}`
|
||||
}
|
||||
|
||||
export function homeContractPath(contract: Contract) {
|
||||
return `/home?c=${contract.slug}`
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import clsx from 'clsx'
|
||||
import React, { useState } from 'react'
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
|
||||
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, useUserById } from 'web/hooks/use-user'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { fromNow } from 'web/lib/util/time'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
|
@ -17,19 +16,17 @@ import {
|
|||
useAcceptedChallenges,
|
||||
useUserChallenges,
|
||||
} from 'web/lib/firebase/challenges'
|
||||
import { Acceptance, Challenge } from 'common/challenge'
|
||||
import { copyToClipboard } from 'web/lib/util/copy'
|
||||
import { ToastClipboard } from 'web/components/toast-clipboard'
|
||||
import { Challenge } from 'common/challenge'
|
||||
import { Tabs } from 'web/components/layout/tabs'
|
||||
import { SiteLink } from 'web/components/site-link'
|
||||
import { UserLink } from 'web/components/user-page'
|
||||
import { Avatar } from 'web/components/avatar'
|
||||
import Router from 'next/router'
|
||||
import { contractPathWithoutContract } from 'web/lib/firebase/contracts'
|
||||
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
export function getManalinkUrl(slug: string) {
|
||||
return `${location.protocol}//${location.host}/link/${slug}`
|
||||
}
|
||||
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 LinkPage() {
|
||||
const user = useUser()
|
||||
|
@ -46,23 +43,18 @@ export default function LinkPage() {
|
|||
<Col className="w-full px-8">
|
||||
<Row className="items-center justify-between">
|
||||
<Title text="Challenges" />
|
||||
{/*{user && (*/}
|
||||
{/* <CreateChallengeButton*/}
|
||||
{/* user={user}*/}
|
||||
{/* />*/}
|
||||
{/*)}*/}
|
||||
</Row>
|
||||
<p>Find or create a question to challenge someone to a bet.</p>
|
||||
<Tabs
|
||||
tabs={[
|
||||
{
|
||||
content: <AllLinksTable links={challenges} />,
|
||||
title: 'All Challenges',
|
||||
content: <PublicChallengesTable links={challenges} />,
|
||||
title: 'Public Challenges',
|
||||
},
|
||||
].concat(
|
||||
user
|
||||
? {
|
||||
content: <LinksTable links={userChallenges} />,
|
||||
content: <YourChallengesTable links={userChallenges} />,
|
||||
title: 'Your Challenges',
|
||||
}
|
||||
: []
|
||||
|
@ -73,149 +65,8 @@ export default function LinkPage() {
|
|||
)
|
||||
}
|
||||
|
||||
function ClaimTableRow(props: { claim: Acceptance }) {
|
||||
const { claim } = props
|
||||
const who = useUserById(claim.userId)
|
||||
return (
|
||||
<tr>
|
||||
<td className="px-5 py-2">{who?.name || 'Loading...'}</td>
|
||||
<td className="px-5 py-2">{`${new Date(
|
||||
claim.createdTime
|
||||
).toLocaleString()}, ${fromNow(claim.createdTime)}`}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function LinkDetailsTable(props: { link: Challenge }) {
|
||||
const { link } = props
|
||||
return (
|
||||
<table className="w-full divide-y divide-gray-300 border border-gray-400">
|
||||
<thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900">
|
||||
<tr>
|
||||
<th className="px-5 py-2">Accepted by</th>
|
||||
<th className="px-5 py-2">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white text-sm text-gray-500">
|
||||
{link.acceptances.length ? (
|
||||
link.acceptances.map((claim) => <ClaimTableRow claim={claim} />)
|
||||
) : (
|
||||
<tr>
|
||||
<td className="px-5 py-2" colSpan={2}>
|
||||
No one's accepted this challenge yet.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
function LinkTableRow(props: { link: Challenge; highlight: boolean }) {
|
||||
const { link, highlight } = props
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<LinkSummaryRow
|
||||
link={link}
|
||||
highlight={highlight}
|
||||
expanded={expanded}
|
||||
onToggle={() => setExpanded((exp) => !exp)}
|
||||
/>
|
||||
{expanded && (
|
||||
<tr>
|
||||
<td className="bg-gray-100 p-3" colSpan={5}>
|
||||
<LinkDetailsTable link={link} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function LinkSummaryRow(props: {
|
||||
link: Challenge
|
||||
highlight: boolean
|
||||
expanded: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
const { link, highlight, expanded, onToggle } = props
|
||||
const [showToast, setShowToast] = useState(false)
|
||||
|
||||
const className = clsx(
|
||||
'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white',
|
||||
highlight ? 'bg-indigo-100 rounded-lg animate-pulse' : ''
|
||||
)
|
||||
return (
|
||||
<tr id={link.slug} key={link.slug} className={className}>
|
||||
<td className="py-4 pl-5" onClick={onToggle}>
|
||||
{expanded ? (
|
||||
<ChevronUpIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-5 w-5" />
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td className="px-5 py-4 font-medium text-gray-900">
|
||||
{formatMoney(link.creatorAmount)}
|
||||
</td>
|
||||
<td
|
||||
className="relative px-5 py-4"
|
||||
onClick={() => {
|
||||
copyToClipboard(getChallengeUrl(link))
|
||||
setShowToast(true)
|
||||
setTimeout(() => setShowToast(false), 3000)
|
||||
}}
|
||||
>
|
||||
{getChallengeUrl(link)
|
||||
.replace('https://manifold.markets', '...')
|
||||
.replace('http://localhost:3000', '...')}
|
||||
{showToast && <ToastClipboard className={'left-10 -top-5'} />}
|
||||
</td>
|
||||
|
||||
<td className="px-5 py-4">
|
||||
{link.acceptedByUserIds.length > 0 ? 'Yes' : 'No'}
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
{link.expiresTime == null ? 'Never' : fromNow(link.expiresTime)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function LinksTable(props: { links: Challenge[]; highlightedSlug?: string }) {
|
||||
const { links, highlightedSlug } = props
|
||||
return links.length == 0 ? (
|
||||
<p>You don't currently have 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></th>
|
||||
<th className="px-5 py-3.5">Amount</th>
|
||||
<th className="px-5 py-3.5">Link</th>
|
||||
<th className="px-5 py-3.5">Accepted</th>
|
||||
<th className="px-5 py-3.5">Expires</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={'divide-y divide-gray-200 bg-white'}>
|
||||
{links.map((link) => (
|
||||
<LinkTableRow
|
||||
link={link}
|
||||
highlight={link.slug === highlightedSlug}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function AllLinksTable(props: {
|
||||
links: Challenge[]
|
||||
highlightedSlug?: string
|
||||
}) {
|
||||
const { links, highlightedSlug } = props
|
||||
function YourChallengesTable(props: { links: Challenge[] }) {
|
||||
const { links } = props
|
||||
return links.length == 0 ? (
|
||||
<p>There aren't currently any challenges.</p>
|
||||
) : (
|
||||
|
@ -223,17 +74,14 @@ function AllLinksTable(props: {
|
|||
<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="px-5 py-3.5">Amount</th>
|
||||
<th className="px-5 py-3.5">Challenge Link</th>
|
||||
<th className="px-5 py-3.5">Accepted By</th>
|
||||
<th className={amountClass}>Amount</th>
|
||||
<th className={columnClass}>Link</th>
|
||||
<th className={columnClass}>Accepted By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={'divide-y divide-gray-200 bg-white'}>
|
||||
{links.map((link) => (
|
||||
<PublicLinkTableRow
|
||||
link={link}
|
||||
highlight={link.slug === highlightedSlug}
|
||||
/>
|
||||
<YourLinkSummaryRow challenge={link} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -241,44 +89,154 @@ function AllLinksTable(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function PublicLinkTableRow(props: { link: Challenge; highlight: boolean }) {
|
||||
const { link, highlight } = props
|
||||
return <PublicLinkSummaryRow link={link} highlight={highlight} />
|
||||
}
|
||||
|
||||
function PublicLinkSummaryRow(props: { link: Challenge; highlight: boolean }) {
|
||||
const { link, highlight } = props
|
||||
function YourLinkSummaryRow(props: { challenge: Challenge }) {
|
||||
const { challenge } = props
|
||||
const { acceptances } = challenge
|
||||
|
||||
const className = clsx(
|
||||
'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white',
|
||||
highlight ? 'bg-indigo-100 rounded-lg animate-pulse' : ''
|
||||
'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white'
|
||||
)
|
||||
return (
|
||||
<tr id={link.slug} key={link.slug} className={className}>
|
||||
<td className="px-5 py-4 font-medium text-gray-900">
|
||||
{formatMoney(link.creatorAmount)}
|
||||
<tr id={challenge.slug} key={challenge.slug} className={className}>
|
||||
<td className={amountClass}>
|
||||
<SiteLink href={getChallengeUrl(challenge)}>
|
||||
{formatMoney(challenge.creatorAmount)}
|
||||
</SiteLink>
|
||||
</td>
|
||||
<td className="relative px-2 py-4">
|
||||
<SiteLink href={getChallengeUrl(link)}>
|
||||
{getChallengeUrl(link)
|
||||
.replace('https://manifold.markets', '...')
|
||||
.replace('http://localhost:3000', '...')}
|
||||
<td className={clsx(columnClass, 'sm:max-w-[150px] lg:max-w-[200px]')}>
|
||||
<SiteLink href={getChallengeUrl(challenge)}>
|
||||
{getChallengeUrl(challenge)
|
||||
.replace(`https://manifold.markets/challenges/`, '...')
|
||||
.replace('http://localhost:3000/challenges', '...')}
|
||||
</SiteLink>
|
||||
</td>
|
||||
|
||||
<td className="px-2 py-4">
|
||||
<td className={columnClass}>
|
||||
<Row className={'items-center justify-start gap-1'}>
|
||||
<Avatar
|
||||
username={link.acceptances[0].userUsername}
|
||||
avatarUrl={link.acceptances[0].userAvatarUrl}
|
||||
size={'sm'}
|
||||
/>
|
||||
<UserLink
|
||||
name={link.acceptances[0].userName}
|
||||
username={link.acceptances[0].userUsername}
|
||||
/>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user