From 8b9d0e5dba401a8e41e4cc6dc504412521f576a3 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 25 Jan 2022 14:47:25 -0600 Subject: [PATCH] New fold page UI with tabs (#37) * Tabbed fold page * Implement market, leaderboard tab views for fold. And edit dialog * Add about field to Fold --- common/fold.ts | 1 + functions/src/create-fold.ts | 8 +- web/components/edit-fold-button.tsx | 128 +++++++++ web/components/fold-back.tsx | 14 - web/components/layout/col.tsx | 15 +- web/components/leaderboard.tsx | 8 +- web/lib/firebase/api-call.ts | 2 +- web/pages/fold/[...slugs]/index.tsx | 312 +++++++++++++++++++++ web/pages/fold/[foldSlug]/edit.tsx | 124 -------- web/pages/fold/[foldSlug]/index.tsx | 142 ---------- web/pages/fold/[foldSlug]/leaderboards.tsx | 102 ------- web/pages/fold/[foldSlug]/markets.tsx | 59 ---- web/pages/folds.tsx | 69 +++-- 13 files changed, 507 insertions(+), 477 deletions(-) create mode 100644 web/components/edit-fold-button.tsx delete mode 100644 web/components/fold-back.tsx create mode 100644 web/pages/fold/[...slugs]/index.tsx delete mode 100644 web/pages/fold/[foldSlug]/edit.tsx delete mode 100644 web/pages/fold/[foldSlug]/index.tsx delete mode 100644 web/pages/fold/[foldSlug]/leaderboards.tsx delete mode 100644 web/pages/fold/[foldSlug]/markets.tsx diff --git a/common/fold.ts b/common/fold.ts index c9df3389..46be7499 100644 --- a/common/fold.ts +++ b/common/fold.ts @@ -2,6 +2,7 @@ export type Fold = { id: string slug: string name: string + about: string curatorId: string // User id createdTime: number diff --git a/functions/src/create-fold.ts b/functions/src/create-fold.ts index eb75a762..a653fa93 100644 --- a/functions/src/create-fold.ts +++ b/functions/src/create-fold.ts @@ -12,6 +12,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall( async ( data: { name: string + about: string tags: string[] }, context @@ -22,7 +23,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall( const creator = await getUser(userId) if (!creator) return { status: 'error', message: 'User not found' } - const { name, tags } = data + const { name, about, tags } = data if (!name || typeof name !== 'string') return { status: 'error', message: 'Name must be a non-empty string' } @@ -35,7 +36,9 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall( creator.username, 'named', name, - 'on', + 'about', + about, + 'tags', tags ) @@ -48,6 +51,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall( curatorId: userId, slug, name, + about, tags, createdTime: Date.now(), contractIds: [], diff --git a/web/components/edit-fold-button.tsx b/web/components/edit-fold-button.tsx new file mode 100644 index 00000000..3ac2504d --- /dev/null +++ b/web/components/edit-fold-button.tsx @@ -0,0 +1,128 @@ +import { useState } from 'react' +import _ from 'lodash' +import clsx from 'clsx' +import { PencilIcon } from '@heroicons/react/outline' + +import { Fold } from '../../common/fold' +import { parseWordsAsTags } from '../../common/util/parse' +import { updateFold } from '../lib/firebase/folds' +import { toCamelCase } from '../lib/util/format' +import { Spacer } from './layout/spacer' +import { TagsList } from './tags-list' + +export function EditFoldButton(props: { fold: Fold }) { + const { fold } = props + const [name, setName] = useState(fold.name) + const [about, setAbout] = useState(fold.about ?? '') + + const initialOtherTags = + fold?.tags.filter((tag) => tag !== toCamelCase(name)).join(', ') ?? '' + + const [otherTags, setOtherTags] = useState(initialOtherTags) + const [isSubmitting, setIsSubmitting] = useState(false) + + const tags = parseWordsAsTags(toCamelCase(name) + ' ' + otherTags) + + const saveDisabled = + name === fold.name && + _.isEqual(tags, fold.tags) && + about === (fold.about ?? '') + + const onSubmit = async () => { + setIsSubmitting(true) + + await updateFold(fold, { + name, + about, + tags, + }) + + setIsSubmitting(false) + } + + return ( +
+ + + +
+
+
+ + + setName(e.target.value || '')} + /> +
+ + + +
+ + + setAbout(e.target.value || '')} + /> +
+ + + +
+ + + setOtherTags(e.target.value || '')} + /> +
+ + + `#${tag}`)} noLink /> + + +
+ + +
+
+
+
+ ) +} diff --git a/web/components/fold-back.tsx b/web/components/fold-back.tsx deleted file mode 100644 index 374e13e3..00000000 --- a/web/components/fold-back.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { ArrowCircleLeftIcon } from '@heroicons/react/outline' -import { Fold } from '../../common/fold' -import { foldPath } from '../lib/firebase/folds' -import { SiteLink } from './site-link' - -export function FoldBack(props: { fold: Fold }) { - const { fold } = props - return ( - - {' '} - {fold.name} - - ) -} diff --git a/web/components/layout/col.tsx b/web/components/layout/col.tsx index 128b13f4..d5f005ca 100644 --- a/web/components/layout/col.tsx +++ b/web/components/layout/col.tsx @@ -1,7 +1,16 @@ import clsx from 'clsx' +import { CSSProperties } from 'react' -export function Col(props: { children?: any; className?: string }) { - const { children, className } = props +export function Col(props: { + children?: any + className?: string + style?: CSSProperties +}) { + const { children, className, style } = props - return
{children}
+ return ( +
+ {children} +
+ ) } diff --git a/web/components/leaderboard.tsx b/web/components/leaderboard.tsx index 27cf1c27..5ab792b1 100644 --- a/web/components/leaderboard.tsx +++ b/web/components/leaderboard.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx' import { User } from '../../common/user' import { Row } from './layout/row' import { SiteLink } from './site-link' @@ -10,10 +11,11 @@ export function Leaderboard(props: { header: string renderCell: (user: User) => any }[] + className?: string }) { - const { title, users, columns } = props + const { title, users, columns, className } = props return ( -
+
<div className="overflow-x-auto"> <table className="table table-zebra table-compact text-gray-500 w-full"> @@ -40,7 +42,7 @@ export function Leaderboard(props: { width={32} height={32} /> - <div>{user.name}</div> + <div className="truncate">{user.name}</div> </Row> </SiteLink> </td> diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 1776e46f..a0fc7de8 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -11,7 +11,7 @@ export const cloudFunction = <RequestData, ResponseData>(name: string) => export const createContract = cloudFunction('createContract') export const createFold = cloudFunction< - { name: string; tags: string[] }, + { name: string; about: string; tags: string[] }, { status: 'error' | 'success'; message?: string; fold?: Fold } >('createFold') diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx new file mode 100644 index 00000000..c73be9b8 --- /dev/null +++ b/web/pages/fold/[...slugs]/index.tsx @@ -0,0 +1,312 @@ +import _ from 'lodash' +import Link from 'next/link' + +import { Fold } from '../../../../common/fold' +import { Comment } from '../../../../common/comment' +import { Page } from '../../../components/page' +import { Title } from '../../../components/title' +import { Bet, listAllBets } from '../../../lib/firebase/bets' +import { listAllComments } from '../../../lib/firebase/comments' +import { Contract } from '../../../lib/firebase/contracts' +import { + foldPath, + getFoldBySlug, + getFoldContracts, +} from '../../../lib/firebase/folds' +import { ActivityFeed, findActiveContracts } from '../../activity' +import { TagsList } from '../../../components/tags-list' +import { Row } from '../../../components/layout/row' +import { UserLink } from '../../../components/user-page' +import { getUser, User } from '../../../lib/firebase/users' +import { Spacer } from '../../../components/layout/spacer' +import { Col } from '../../../components/layout/col' +import { useUser } from '../../../hooks/use-user' +import { useFold } from '../../../hooks/use-fold' +import { SearchableGrid } from '../../../components/contracts-list' +import { useQueryAndSortParams } from '../../../hooks/use-sort-and-query-params' +import { useRouter } from 'next/router' +import clsx from 'clsx' +import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring' +import { Leaderboard } from '../../../components/leaderboard' +import { formatMoney } from '../../../lib/util/format' +import { EditFoldButton } from '../../../components/edit-fold-button' + +export async function getStaticProps(props: { params: { slugs: string[] } }) { + const { slugs } = props.params + + const fold = await getFoldBySlug(slugs[0]) + const curatorPromise = fold ? getUser(fold.curatorId) : null + + const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : [] + const contractComments = await Promise.all( + contracts.map((contract) => listAllComments(contract.id).catch((_) => [])) + ) + + let activeContracts = findActiveContracts( + contracts, + _.flatten(contractComments), + 365 + ) + const [resolved, unresolved] = _.partition( + activeContracts, + ({ isResolved }) => isResolved + ) + activeContracts = [...unresolved, ...resolved] + + const activeContractBets = await Promise.all( + activeContracts.map((contract) => listAllBets(contract.id).catch((_) => [])) + ) + const activeContractComments = activeContracts.map( + (contract) => + contractComments[contracts.findIndex((c) => c.id === contract.id)] + ) + + const curator = await curatorPromise + + const bets = await Promise.all( + contracts.map((contract) => listAllBets(contract.id)) + ) + + const creatorScores = scoreCreators(contracts, bets) + const [topCreators, topCreatorScores] = await toUserScores(creatorScores) + + const traderScores = scoreTraders(contracts, bets) + const [topTraders, topTraderScores] = await toUserScores(traderScores) + + return { + props: { + fold, + curator, + contracts, + activeContracts, + activeContractBets, + activeContractComments, + topTraders, + topTraderScores, + topCreators, + topCreatorScores, + }, + + revalidate: 60, // regenerate after a minute + } +} + +async function toUserScores(userScores: { [userId: string]: number }) { + const topUserPairs = _.take( + _.sortBy(Object.entries(userScores), ([_, score]) => -1 * score), + 10 + ) + const topUsers = await Promise.all( + topUserPairs.map(([userId]) => getUser(userId)) + ) + const topUserScores = topUserPairs.map(([_, score]) => score) + return [topUsers, topUserScores] as const +} + +export async function getStaticPaths() { + return { paths: [], fallback: 'blocking' } +} + +export default function FoldPage(props: { + fold: Fold + curator: User + contracts: Contract[] + activeContracts: Contract[] + activeContractBets: Bet[][] + activeContractComments: Comment[][] + topTraders: User[] + topTraderScores: number[] + topCreators: User[] + topCreatorScores: number[] + params: { tab: string } +}) { + const { + curator, + contracts, + activeContracts, + activeContractBets, + activeContractComments, + topTraders, + topTraderScores, + topCreators, + topCreatorScores, + } = props + + const router = useRouter() + const { slugs } = router.query as { slugs: string[] } + const page = + (slugs[1] as 'markets' | 'leaderboards' | undefined) ?? 'activity' + + const fold = useFold(props.fold.id) ?? props.fold + const { curatorId } = fold + + const { query, setQuery, sort, setSort } = useQueryAndSortParams({ + defaultSort: 'most-traded', + }) + + const user = useUser() + const isCurator = user?.id === curatorId + + return ( + <Page wide> + <Col className="items-center"> + <Col className="max-w-5xl w-full"> + <Col className="sm:flex-row sm:justify-between sm:items-end gap-4 mb-6"> + <Title className="!m-0" text={fold.name} /> + {isCurator && <EditFoldButton fold={fold} />} + </Col> + + <div className="tabs mb-4"> + <Link href={foldPath(fold)} shallow> + <a + className={clsx( + 'tab tab-bordered', + page === 'activity' && 'tab-active' + )} + > + Activity + </a> + </Link> + + <Link href={foldPath(fold, 'markets')} shallow> + <a + className={clsx( + 'tab tab-bordered', + page === 'markets' && 'tab-active' + )} + > + Markets + </a> + </Link> + <Link href={foldPath(fold, 'leaderboards')} shallow> + <a + className={clsx( + 'tab tab-bordered', + page === 'leaderboards' && 'tab-active', + page !== 'leaderboards' && 'md:hidden' + )} + > + Leaderboards + </a> + </Link> + </div> + + {page === 'activity' && ( + <Row className="gap-8"> + <Col> + <ActivityFeed + contracts={activeContracts} + contractBets={activeContractBets} + contractComments={activeContractComments} + /> + </Col> + <Col className="hidden md:flex max-w-xs gap-10"> + <FoldOverview fold={fold} curator={curator} /> + <FoldLeaderboards + topTraders={topTraders} + topTraderScores={topTraderScores} + topCreators={topCreators} + topCreatorScores={topCreatorScores} + /> + </Col> + </Row> + )} + + {page === 'markets' && ( + <div className="w-full"> + <SearchableGrid + contracts={contracts} + query={query} + setQuery={setQuery} + sort={sort} + setSort={setSort} + /> + </div> + )} + + {page === 'leaderboards' && ( + <Col className="gap-8"> + <FoldLeaderboards + topTraders={topTraders} + topTraderScores={topTraderScores} + topCreators={topCreators} + topCreatorScores={topCreatorScores} + /> + </Col> + )} + </Col> + </Col> + </Page> + ) +} + +function FoldOverview(props: { fold: Fold; curator: User }) { + const { fold, curator } = props + const { about, tags } = fold + + return ( + <Col className="max-w-sm"> + <div className="px-4 py-3 bg-indigo-700 text-white text-sm rounded-t"> + About community + </div> + <Col className="p-4 bg-white self-start gap-2 rounded-b"> + <Row> + <div className="text-gray-500 mr-1">Curated by</div> + <UserLink + className="text-neutral" + name={curator.name} + username={curator.username} + /> + </Row> + + {about && ( + <> + <Spacer h={2} /> + <div className="text-gray-500">{about}</div> + </> + )} + + <Spacer h={2} /> + + <TagsList tags={tags.map((tag) => `#${tag}`)} /> + </Col> + </Col> + ) +} + +function FoldLeaderboards(props: { + topTraders: User[] + topTraderScores: number[] + topCreators: User[] + topCreatorScores: number[] +}) { + const { topTraders, topTraderScores, topCreators, topCreatorScores } = props + return ( + <> + <Leaderboard + className="max-w-xl" + title="🏅 Top traders" + users={topTraders} + columns={[ + { + header: 'Profit', + renderCell: (user) => + formatMoney(topTraderScores[topTraders.indexOf(user)]), + }, + ]} + /> + <Leaderboard + className="max-w-xl" + title="🏅 Top creators" + users={topCreators} + columns={[ + { + header: 'Market pool', + renderCell: (user) => + formatMoney(topCreatorScores[topCreators.indexOf(user)]), + }, + ]} + /> + </> + ) +} diff --git a/web/pages/fold/[foldSlug]/edit.tsx b/web/pages/fold/[foldSlug]/edit.tsx deleted file mode 100644 index c89992ed..00000000 --- a/web/pages/fold/[foldSlug]/edit.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import clsx from 'clsx' -import _ from 'lodash' -import { ArrowCircleLeftIcon } from '@heroicons/react/solid' -import { useState } from 'react' -import { Fold } from '../../../../common/fold' -import { parseWordsAsTags } from '../../../../common/util/parse' -import { Col } from '../../../components/layout/col' -import { Spacer } from '../../../components/layout/spacer' -import { Page } from '../../../components/page' -import { TagsList } from '../../../components/tags-list' -import { - foldPath, - getFoldBySlug, - updateFold, -} from '../../../lib/firebase/folds' -import Custom404 from '../../404' -import { SiteLink } from '../../../components/site-link' -import { toCamelCase } from '../../../lib/util/format' -import { useFold } from '../../../hooks/use-fold' - -export async function getStaticProps(props: { params: { foldSlug: string } }) { - const { foldSlug } = props.params - - const fold = await getFoldBySlug(foldSlug) - - return { - props: { fold }, - - revalidate: 60, // regenerate after a minute - } -} - -export async function getStaticPaths() { - return { paths: [], fallback: 'blocking' } -} - -export default function EditFoldPage(props: { fold: Fold | null }) { - const fold = useFold(props.fold?.id ?? '') ?? props.fold - const [name, setName] = useState(fold?.name ?? '') - - const initialOtherTags = - fold?.tags.filter((tag) => tag !== toCamelCase(name)).join(', ') ?? '' - - const [otherTags, setOtherTags] = useState(initialOtherTags) - const [isSubmitting, setIsSubmitting] = useState(false) - - if (!fold) return <Custom404 /> - - const tags = parseWordsAsTags(toCamelCase(name) + ' ' + otherTags) - - const saveDisabled = - !name || (name === fold.name && _.isEqual(tags, fold.tags)) - - const onSubmit = async () => { - setIsSubmitting(true) - - await updateFold(fold, { - name, - tags, - }) - - setIsSubmitting(false) - } - - return ( - <Page> - <Col className="items-center"> - <Col className="max-w-2xl w-full px-2 sm:px-0"> - <SiteLink href={foldPath(fold)}> - <ArrowCircleLeftIcon className="h-5 w-5 text-gray-500 inline mr-1" />{' '} - {fold.name} - </SiteLink> - - <Spacer h={4} /> - - <div className="form-control w-full"> - <label className="label"> - <span className="mb-1">Fold name</span> - </label> - - <input - placeholder="Your fold name" - className="input input-bordered resize-none" - disabled={isSubmitting} - value={name} - onChange={(e) => setName(e.target.value || '')} - /> - </div> - - <Spacer h={4} /> - - <div className="form-control w-full"> - <label className="label"> - <span className="mb-1">Tags</span> - </label> - - <input - placeholder="Politics, Economics, Rationality" - className="input input-bordered resize-none" - disabled={isSubmitting} - value={otherTags} - onChange={(e) => setOtherTags(e.target.value || '')} - /> - </div> - - <Spacer h={4} /> - <TagsList tags={tags.map((tag) => `#${tag}`)} noLink /> - <Spacer h={4} /> - - <button - className={clsx( - 'btn self-end', - saveDisabled ? 'btn-disabled' : 'btn-primary', - isSubmitting && 'loading' - )} - onClick={onSubmit} - > - Save - </button> - </Col> - </Col> - </Page> - ) -} diff --git a/web/pages/fold/[foldSlug]/index.tsx b/web/pages/fold/[foldSlug]/index.tsx deleted file mode 100644 index 593a8199..00000000 --- a/web/pages/fold/[foldSlug]/index.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import _ from 'lodash' -import { Fold } from '../../../../common/fold' -import { Comment } from '../../../../common/comment' -import { Page } from '../../../components/page' -import { Title } from '../../../components/title' -import { Bet, listAllBets } from '../../../lib/firebase/bets' -import { listAllComments } from '../../../lib/firebase/comments' -import { Contract } from '../../../lib/firebase/contracts' -import { - foldPath, - getFoldBySlug, - getFoldContracts, -} from '../../../lib/firebase/folds' -import { ActivityFeed, findActiveContracts } from '../../activity' -import { TagsList } from '../../../components/tags-list' -import { Row } from '../../../components/layout/row' -import { UserLink } from '../../../components/user-page' -import { getUser, User } from '../../../lib/firebase/users' -import { Spacer } from '../../../components/layout/spacer' -import { Col } from '../../../components/layout/col' -import { SiteLink } from '../../../components/site-link' -import { useUser } from '../../../hooks/use-user' -import { useFold } from '../../../hooks/use-fold' - -export async function getStaticProps(props: { params: { foldSlug: string } }) { - const { foldSlug } = props.params - - const fold = await getFoldBySlug(foldSlug) - const curatorPromise = fold ? getUser(fold.curatorId) : null - - const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : [] - const contractComments = await Promise.all( - contracts.map((contract) => listAllComments(contract.id).catch((_) => [])) - ) - - let activeContracts = findActiveContracts( - contracts, - _.flatten(contractComments), - 365 - ) - const [resolved, unresolved] = _.partition( - activeContracts, - ({ isResolved }) => isResolved - ) - activeContracts = [...unresolved, ...resolved] - - const activeContractBets = await Promise.all( - activeContracts.map((contract) => listAllBets(contract.id).catch((_) => [])) - ) - const activeContractComments = activeContracts.map( - (contract) => - contractComments[contracts.findIndex((c) => c.id === contract.id)] - ) - - const curator = await curatorPromise - - return { - props: { - fold, - curator, - activeContracts, - activeContractBets, - activeContractComments, - }, - - revalidate: 60, // regenerate after a minute - } -} - -export async function getStaticPaths() { - return { paths: [], fallback: 'blocking' } -} - -export default function FoldPage(props: { - fold: Fold - curator: User - activeContracts: Contract[] - activeContractBets: Bet[][] - activeContractComments: Comment[][] -}) { - const { - curator, - activeContracts, - activeContractBets, - activeContractComments, - } = props - - const fold = useFold(props.fold.id) ?? props.fold - const { tags, curatorId } = fold - - const user = useUser() - const isCurator = user?.id === curatorId - - return ( - <Page> - <Col className="items-center"> - <Col className="max-w-3xl w-full"> - <Title className="!mt-0" text={fold.name} /> - - <Row className="items-center gap-2 mb-2 flex-wrap"> - <SiteLink className="text-sm" href={foldPath(fold, 'markets')}> - Markets - </SiteLink> - <div className="text-gray-500">•</div> - <SiteLink className="text-sm" href={foldPath(fold, 'leaderboards')}> - Leaderboards - </SiteLink> - <div className="text-gray-500">•</div> - <Row> - <div className="text-sm text-gray-500 mr-1">Curated by</div> - <UserLink - className="text-sm text-neutral" - name={curator.name} - username={curator.username} - /> - </Row> - {isCurator && ( - <> - <div className="text-gray-500">•</div> - <SiteLink className="text-sm " href={foldPath(fold, 'edit')}> - Edit - </SiteLink> - </> - )} - </Row> - - <Spacer h={2} /> - - <TagsList tags={tags.map((tag) => `#${tag}`)} /> - - <Spacer h={8} /> - - <ActivityFeed - contracts={activeContracts} - contractBets={activeContractBets} - contractComments={activeContractComments} - /> - </Col> - </Col> - </Page> - ) -} diff --git a/web/pages/fold/[foldSlug]/leaderboards.tsx b/web/pages/fold/[foldSlug]/leaderboards.tsx deleted file mode 100644 index ea89a5ed..00000000 --- a/web/pages/fold/[foldSlug]/leaderboards.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import _ from 'lodash' -import { ArrowCircleLeftIcon } from '@heroicons/react/solid' - -import { Col } from '../../../components/layout/col' -import { Leaderboard } from '../../../components/leaderboard' -import { Page } from '../../../components/page' -import { SiteLink } from '../../../components/site-link' -import { formatMoney } from '../../../lib/util/format' -import { - foldPath, - getFoldBySlug, - getFoldContracts, -} from '../../../lib/firebase/folds' -import { Fold } from '../../../../common/fold' -import { Spacer } from '../../../components/layout/spacer' -import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring' -import { getUser, User } from '../../../lib/firebase/users' -import { listAllBets } from '../../../lib/firebase/bets' - -export async function getStaticProps(props: { params: { foldSlug: string } }) { - const { foldSlug } = props.params - - const fold = await getFoldBySlug(foldSlug) - const contracts = fold ? await getFoldContracts(fold) : [] - const bets = await Promise.all( - contracts.map((contract) => listAllBets(contract.id)) - ) - - const creatorScores = scoreCreators(contracts, bets) - const [topCreators, topCreatorScores] = await toUserScores(creatorScores) - - const traderScores = scoreTraders(contracts, bets) - const [topTraders, topTraderScores] = await toUserScores(traderScores) - - return { - props: { fold, topTraders, topTraderScores, topCreators, topCreatorScores }, - - revalidate: 60, // regenerate after 60 seconds - } -} - -export async function getStaticPaths() { - return { paths: [], fallback: 'blocking' } -} - -async function toUserScores(userScores: { [userId: string]: number }) { - const topUserPairs = _.take( - _.sortBy(Object.entries(userScores), ([_, score]) => -1 * score), - 10 - ) - const topUsers = await Promise.all( - topUserPairs.map(([userId]) => getUser(userId)) - ) - const topUserScores = topUserPairs.map(([_, score]) => score) - return [topUsers, topUserScores] as const -} - -export default function Leaderboards(props: { - fold: Fold - topTraders: User[] - topTraderScores: number[] - topCreators: User[] - topCreatorScores: number[] -}) { - const { fold, topTraders, topTraderScores, topCreators, topCreatorScores } = - props - return ( - <Page> - <SiteLink href={foldPath(fold)}> - <ArrowCircleLeftIcon className="h-5 w-5 text-gray-500 inline mr-1" />{' '} - {fold.name} - </SiteLink> - - <Spacer h={4} /> - - <Col className="lg:flex-row gap-10"> - <Leaderboard - title="🏅 Top traders" - users={topTraders} - columns={[ - { - header: 'Total profit', - renderCell: (user) => - formatMoney(topTraderScores[topTraders.indexOf(user)]), - }, - ]} - /> - <Leaderboard - title="🏅 Top creators" - users={topCreators} - columns={[ - { - header: 'Market pool', - renderCell: (user) => - formatMoney(topCreatorScores[topCreators.indexOf(user)]), - }, - ]} - /> - </Col> - </Page> - ) -} diff --git a/web/pages/fold/[foldSlug]/markets.tsx b/web/pages/fold/[foldSlug]/markets.tsx deleted file mode 100644 index 667cb68a..00000000 --- a/web/pages/fold/[foldSlug]/markets.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import _ from 'lodash' -import { Contract } from '../../../../common/contract' -import { Fold } from '../../../../common/fold' -import { SearchableGrid } from '../../../components/contracts-list' -import { FoldBack } from '../../../components/fold-back' -import { Spacer } from '../../../components/layout/spacer' -import { Page } from '../../../components/page' -import { SEO } from '../../../components/SEO' -import { useQueryAndSortParams } from '../../../hooks/use-sort-and-query-params' -import { getFoldBySlug, getFoldContracts } from '../../../lib/firebase/folds' - -export async function getStaticProps(props: { params: { foldSlug: string } }) { - const { foldSlug } = props.params - - const fold = await getFoldBySlug(foldSlug) - const contracts = fold ? await getFoldContracts(fold) : [] - - return { - props: { - fold, - contracts, - }, - - revalidate: 60, // regenerate after a minute - } -} - -export async function getStaticPaths() { - return { paths: [], fallback: 'blocking' } -} - -export default function Markets(props: { fold: Fold; contracts: Contract[] }) { - const { fold, contracts } = props - const { query, setQuery, sort, setSort } = useQueryAndSortParams({ - defaultSort: 'most-traded', - }) - - return ( - <Page> - <SEO - title={`${fold.name}'s markets`} - description={`Explore or search all the markets of ${fold.name}`} - url="/markets" - /> - - <FoldBack fold={fold} /> - - <Spacer h={4} /> - - <SearchableGrid - contracts={contracts} - query={query} - setQuery={setQuery} - sort={sort} - setSort={setSort} - /> - </Page> - ) -} diff --git a/web/pages/folds.tsx b/web/pages/folds.tsx index 7d86832a..d6be19e9 100644 --- a/web/pages/folds.tsx +++ b/web/pages/folds.tsx @@ -103,6 +103,7 @@ export default function Folds(props: { function CreateFoldButton() { const [name, setName] = useState('') + const [about, setAbout] = useState('') const [otherTags, setOtherTags] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) @@ -120,6 +121,7 @@ function CreateFoldButton() { const result = await createFold({ name, tags, + about, }).then((r) => r.data || {}) if (result.fold) { @@ -145,7 +147,7 @@ function CreateFoldButton() { }} submitBtn={{ label: 'Create', - className: clsx(name ? 'btn-primary' : 'btn-disabled'), + className: clsx(name && about ? 'btn-primary' : 'btn-disabled'), }} onSubmit={onSubmit} > @@ -175,37 +177,50 @@ function CreateFoldButton() { <Spacer h={4} /> - {name && ( - <> - <label className="label"> - <span className="mb-1">Primary tag</span> - </label> - <TagsList noLink tags={[`#${toCamelCase(name)}`]} /> + <div className="form-control w-full"> + <label className="label"> + <span className="mb-1">About</span> + </label> - <Spacer h={4} /> + <input + placeholder="Short description (140 characters max)" + className="input input-bordered resize-none" + disabled={isSubmitting} + value={about} + maxLength={140} + onChange={(e) => setAbout(e.target.value || '')} + /> + </div> - <div className="form-control w-full"> - <label className="label"> - <span className="mb-1">Additional tags</span> - </label> + <Spacer h={4} /> - <input - placeholder="Politics, Economics, Rationality" - className="input input-bordered resize-none" - disabled={isSubmitting} - value={otherTags} - onChange={(e) => setOtherTags(e.target.value || '')} - /> - </div> + <label className="label"> + <span className="mb-1">Primary tag</span> + </label> + <TagsList noLink tags={[`#${toCamelCase(name)}`]} /> - <Spacer h={4} /> + <Spacer h={4} /> - <TagsList - tags={parseWordsAsTags(otherTags).map((tag) => `#${tag}`)} - noLink - /> - </> - )} + <div className="form-control w-full"> + <label className="label"> + <span className="mb-1">Additional tags</span> + </label> + + <input + placeholder="Politics, Economics, Rationality (Optional)" + className="input input-bordered resize-none" + disabled={isSubmitting} + value={otherTags} + onChange={(e) => setOtherTags(e.target.value || '')} + /> + </div> + + <Spacer h={4} /> + + <TagsList + tags={parseWordsAsTags(otherTags).map((tag) => `#${tag}`)} + noLink + /> </div> </ConfirmationButton> )