From 7cc4710ab10b6cabe3ffafb24eee671fb8d3d1c8 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Thu, 4 Aug 2022 10:25:35 -0600 Subject: [PATCH] Restyle challenges list, cache contract name --- common/challenge.ts | 2 + web/lib/firebase/challenges.ts | 2 + web/lib/firebase/contracts.ts | 7 + web/pages/challenges/index.tsx | 348 +++++++++++++++------------------ 4 files changed, 164 insertions(+), 195 deletions(-) diff --git a/common/challenge.ts b/common/challenge.ts index 344c765f..3227f9b3 100644 --- a/common/challenge.ts +++ b/common/challenge.ts @@ -27,6 +27,8 @@ export type Challenge = { contractId: string contractSlug: string + contractQuestion: string + contractCreatorUsername: string createdTime: number // If null, the link is valid forever diff --git a/web/lib/firebase/challenges.ts b/web/lib/firebase/challenges.ts index f3a034cb..d62d5aac 100644 --- a/web/lib/firebase/challenges.ts +++ b/web/lib/firebase/challenges.ts @@ -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, diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 9e5de871..3a751c18 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -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}` } diff --git a/web/pages/challenges/index.tsx b/web/pages/challenges/index.tsx index 0d1ee039..5c6a7640 100644 --- a/web/pages/challenges/index.tsx +++ b/web/pages/challenges/index.tsx @@ -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() { - {/*{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> + ) +}